863 lines
27 KiB
TypeScript
863 lines
27 KiB
TypeScript
/* eslint-disable react-refresh/only-export-components */
|
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
import {
|
|
type CellArray,
|
|
CompactSelection,
|
|
type DataEditorProps,
|
|
type DataEditorRef,
|
|
type EditableGridCell,
|
|
type GetCellsThunk,
|
|
type GridCell,
|
|
GridCellKind,
|
|
type GridColumn,
|
|
type GridMouseEventArgs,
|
|
type GridSelection,
|
|
type HeaderClickedEventArgs,
|
|
type Item,
|
|
type Rectangle,
|
|
} from '@glideapps/glide-data-grid';
|
|
import { IconGrid4x4 } from '@tabler/icons-react';
|
|
import { getUUID } from '@warkypublic/artemis-kit';
|
|
import { getNestedValue } from '@warkypublic/artemis-kit/object';
|
|
import { createSyncStore } from '@warkypublic/zustandsyncstore';
|
|
import { produce } from 'immer';
|
|
import { type PropsWithChildren, type ReactNode, useEffect } from 'react';
|
|
|
|
import {
|
|
type MantineBetterMenuInstance,
|
|
type MantineBetterMenuInstanceItem,
|
|
useMantineBetterMenus,
|
|
} from '../../MantineBetterMenu';
|
|
import { ColumnFilterSet, type GridlerColumn, type GridlerColumns } from './Column';
|
|
import { SortDownSprite } from './sprites/SortDown';
|
|
import { SortUpSprite } from './sprites/SortUp';
|
|
import { SpriteImage } from './sprites/SpriteImage';
|
|
|
|
export type FilterOption = {
|
|
datatype?: 'array' | 'boolean' | 'date' | 'function' | 'number' | 'object' | 'string';
|
|
id: string;
|
|
operator: FilterOptionOperator;
|
|
value: string;
|
|
value2?: string;
|
|
};
|
|
export type FilterOptionOperator =
|
|
| 'between'
|
|
| 'contains'
|
|
| 'endswith'
|
|
| 'eq'
|
|
| 'gt'
|
|
| 'gte'
|
|
| 'lt'
|
|
| 'lte'
|
|
| 'neq'
|
|
| 'startswith';
|
|
|
|
export interface GridlerProps extends PropsWithChildren {
|
|
askAPIRowNumber?: (key: string) => Promise<number>;
|
|
columns?: GridlerColumns;
|
|
|
|
defaultSort?: Array<SortOption>;
|
|
enableOddEvenRowColor?: boolean;
|
|
getMenuItems?: (
|
|
id: string,
|
|
storeState: GridlerState,
|
|
row?: any,
|
|
col?: GridlerColumn,
|
|
defaultItems?: Array<MantineBetterMenuInstanceItem>
|
|
) => Array<MantineBetterMenuInstanceItem>;
|
|
|
|
glideProps?: Partial<DataEditorProps>;
|
|
headerHeight?: number;
|
|
height?: number | string;
|
|
hideMenu?: (id: string) => void;
|
|
keyField?: string;
|
|
maxConcurrency?: number;
|
|
onChange?: (values: Array<Record<string, any>>) => void;
|
|
onMounted?: (getState: GridlerState['getState'], setState: GridlerState['setState']) => void;
|
|
onUnMounted?: () => void;
|
|
pageSize?: number;
|
|
progressiveScroll?: boolean;
|
|
RenderCell?: <TRowType extends Record<string, string>>(
|
|
row: TRowType,
|
|
colindex: number,
|
|
colid: string,
|
|
value: any,
|
|
store: GridlerState
|
|
) => GridCell;
|
|
|
|
rowHeight?: number;
|
|
sections?: {
|
|
bottom?: React.ReactNode;
|
|
left?: React.ReactNode;
|
|
right?: React.ReactNode;
|
|
rightElementEnd?: React.ReactNode;
|
|
rightElementStart?: React.ReactNode;
|
|
top?: React.ReactNode;
|
|
};
|
|
selectedRow?: number;
|
|
selectMode?: 'cell' | 'row';
|
|
showMenu?: (id: string, options?: Partial<MantineBetterMenuInstance>) => void;
|
|
tooltipBarProps?: React.HTMLAttributes<HTMLDivElement>;
|
|
total_rows?: number;
|
|
uniqueid: string;
|
|
useAPIQuery?: (index: number) => Promise<Array<Record<string, any>>>;
|
|
values?: Array<Record<string, any>>;
|
|
width?: number | string;
|
|
}
|
|
|
|
export interface GridlerState {
|
|
_active_requests?: Array<{ controller: AbortController; page: number }>;
|
|
_activeTooltip?: ReactNode;
|
|
_events: EventTarget;
|
|
_glideref?: DataEditorRef;
|
|
_gridSelection?: GridSelection;
|
|
_gridSelectionRows?: GridSelection['rows'];
|
|
_loadingList: CompactSelection;
|
|
_page_data: Record<number, Array<any>>;
|
|
_scrollTimeout?: any | number;
|
|
_visibleArea: Rectangle;
|
|
_visiblePages: Rectangle;
|
|
|
|
addError: (err: string, ...args: Array<any>) => void;
|
|
colFilters?: Array<FilterOption>;
|
|
colOrder?: Record<string, number>;
|
|
colSize?: Record<string, number>;
|
|
colSort?: Array<SortOption>;
|
|
data?: Array<any>;
|
|
|
|
errors: Array<string>;
|
|
focused?: boolean;
|
|
get: () => GridlerState;
|
|
getCellContent: (cell: Item) => GridCell;
|
|
getCellsForSelection: (
|
|
selection: Rectangle,
|
|
abortSignal: AbortSignal
|
|
) => CellArray | GetCellsThunk;
|
|
getRowBuffer: (row: number) => any;
|
|
getState: <K extends keyof GridlerStoreState>(key: K) => GridlerStoreState[K];
|
|
|
|
hasLocalData: boolean;
|
|
loadingData?: boolean;
|
|
loadPage: (page: number, clearMode?: 'all' | 'page') => Promise<void>;
|
|
mounted: boolean;
|
|
onCellEdited: (cell: Item, newVal: EditableGridCell) => void;
|
|
onColumnMoved: (from: number, to: number) => void;
|
|
onColumnProposeMove: (startIndex: number, endIndex: number) => boolean;
|
|
onColumnResize: (
|
|
column: GridColumn,
|
|
newSize: number,
|
|
colIndex: number,
|
|
newSizeWithGrow: number
|
|
) => void;
|
|
onContextClick: (area: string, event: any, col?: number, row?: number) => void;
|
|
onHeaderClicked: (colIndex: number, event: HeaderClickedEventArgs) => void;
|
|
onHeaderMenuClick: (col: number, screenPosition: Rectangle) => void;
|
|
onItemHovered: (args: GridMouseEventArgs) => void;
|
|
|
|
onVisibleRegionChanged: (
|
|
r: Rectangle,
|
|
tx: number,
|
|
ty: number,
|
|
extras: {
|
|
freezeRegion?: Rectangle;
|
|
freezeRegions?: readonly Rectangle[];
|
|
selected?: Item;
|
|
}
|
|
) => void;
|
|
|
|
pageSize: number;
|
|
reload?: () => Promise<void>;
|
|
renderColumns?: GridlerColumns;
|
|
setState: <K extends keyof GridlerStoreState>(
|
|
key: K,
|
|
value: Partial<GridlerStoreState[K]>
|
|
) => void;
|
|
setStateFN: <K extends keyof GridlerStoreState>(
|
|
key: K,
|
|
value: (current: GridlerStoreState[K]) => Partial<GridlerStoreState[K]>
|
|
) => Promise<void>;
|
|
toCell: <TRowType extends Record<string, string>>(row: TRowType, col: number) => GridCell;
|
|
}
|
|
|
|
export type GridlerStoreState = GridlerProps & GridlerState;
|
|
|
|
export type SortOption = { direction: 'asc' | 'desc'; id: string; order?: number };
|
|
|
|
const { Provider, useStore: useGridlerStore } = createSyncStore<GridlerStoreState, GridlerProps>(
|
|
(set, get) => ({
|
|
_events: new EventTarget(),
|
|
_loadingList: CompactSelection.empty(),
|
|
_page_data: {},
|
|
_visibleArea: { height: 10000, width: 1000, x: 0, y: 0 },
|
|
_visiblePages: { height: 0, width: 0, x: 0, y: 0 },
|
|
addError: (err: string, ...args: Array<any>) => {
|
|
const s = get();
|
|
console.log('Gridler Error', s.uniqueid, err, args);
|
|
set(
|
|
produce((state: GridlerStoreState) => {
|
|
state.errors = [...state.errors, err];
|
|
})
|
|
);
|
|
},
|
|
errors: [],
|
|
get: () => get(),
|
|
getCellContent: (cell: Item) => {
|
|
const state = get();
|
|
const [col, row] = cell;
|
|
|
|
const buffer = state.getRowBuffer(row);
|
|
|
|
if (buffer !== undefined) {
|
|
return state.toCell(buffer, col);
|
|
}
|
|
return {
|
|
allowOverlay: false,
|
|
kind: GridCellKind.Loading,
|
|
};
|
|
},
|
|
getCellsForSelection: (selection: Rectangle, _abortSignal: AbortSignal) => {
|
|
return async () => {
|
|
const state = get();
|
|
//const firstPage = Math.max(0, Math.floor(selection.y / state.pageSize));
|
|
//const lastPage = Math.floor((selection.y + selection.height) / state.pageSize);
|
|
|
|
await state.setStateFN('_visibleArea', (_cv) => {
|
|
//if (r.x === cv.x && r.y === cv.y && r.width === cv.width && r.height === cv.height)
|
|
// return cv;
|
|
return selection;
|
|
});
|
|
|
|
const result: GridCell[][] = [];
|
|
|
|
for (let y = selection.y; y < selection.y + selection.height; y++) {
|
|
const row: GridCell[] = [];
|
|
for (let x = selection.x; x < selection.x + selection.width; x++) {
|
|
row.push(state.getCellContent([x, y]));
|
|
}
|
|
result.push(row);
|
|
}
|
|
|
|
//console.log('Gridler:Debug:getCellsForSelection', selection, result);
|
|
return result as CellArray;
|
|
};
|
|
},
|
|
getRowBuffer: (row: number) => {
|
|
const state = get();
|
|
//Handle local data
|
|
if (state.data && state.data.length > 0) {
|
|
if (state.data[row] === undefined) {
|
|
return {
|
|
allowOverlay: false,
|
|
kind: GridCellKind.Loading,
|
|
};
|
|
}
|
|
return state.data[row];
|
|
}
|
|
|
|
//Handle remote paged data
|
|
const firstPage = Math.max(0, Math.floor(row / state.pageSize));
|
|
const upperPage = state.pageSize * firstPage;
|
|
const index = row - upperPage;
|
|
|
|
const rowData = state._page_data?.[firstPage]?.[index];
|
|
|
|
return rowData;
|
|
},
|
|
getState: (key) => {
|
|
return get()[key];
|
|
},
|
|
hasLocalData: false,
|
|
keyField: 'id',
|
|
loadPage: async (pPage: number, clearMode?: 'all' | 'page') => {
|
|
const state = get();
|
|
const page = pPage < 0 ? 0 : pPage;
|
|
|
|
const result = state._events.dispatchEvent(
|
|
new CustomEvent('before_loadPage', {
|
|
detail: { clearMode, page: pPage, state },
|
|
})
|
|
);
|
|
if (!result) {
|
|
return;
|
|
}
|
|
|
|
const damageList: { cell: [number, number] }[] = [];
|
|
const colLen = Object.keys(state.renderColumns ?? [1, 2, 3]).length;
|
|
const upperPage = state.pageSize * page;
|
|
const hasPage = state._page_data?.[page]?.length > 0;
|
|
//console.log('loadPage', page, clearMode, { upperPage, hasPage, pz: state.pageSize, colLen });
|
|
if (clearMode === 'all') {
|
|
state._active_requests?.forEach((r) => {
|
|
r.controller?.abort?.();
|
|
});
|
|
|
|
state.setState('_page_data', {});
|
|
state.setState('_active_requests', []);
|
|
|
|
state.loadPage(page);
|
|
return;
|
|
}
|
|
|
|
if (!state.useAPIQuery) {
|
|
console.warn('No useAPIQuery function defined, cannot load page', page);
|
|
return;
|
|
}
|
|
if (!hasPage && clearMode !== 'page') {
|
|
state
|
|
.useAPIQuery?.(page)
|
|
.then((data) => {
|
|
state.setStateFN('_page_data', (cv) => {
|
|
return { ...cv, [page]: data };
|
|
});
|
|
for (let row = page; row <= upperPage + state.pageSize; row++) {
|
|
for (let col = 0; col <= colLen; col++) {
|
|
damageList.push({
|
|
cell: [col, row],
|
|
});
|
|
}
|
|
}
|
|
|
|
state._glideref?.updateCells(damageList);
|
|
state._events.dispatchEvent(
|
|
new CustomEvent('loadPage', {
|
|
detail: { clearMode, data, page: pPage, state },
|
|
})
|
|
);
|
|
})
|
|
.catch((e) => {
|
|
console.warn('loadPage Error: ', page, e);
|
|
state._events.dispatchEvent(
|
|
new CustomEvent('loadPage_error', {
|
|
detail: { clearMode, error: e, page: pPage, state },
|
|
})
|
|
);
|
|
});
|
|
}
|
|
},
|
|
maxConcurrency: 1,
|
|
mounted: false,
|
|
onCellEdited: (cell: Item, newVal: EditableGridCell) => {
|
|
const state = get();
|
|
const [, row] = cell;
|
|
//const current = state._editData?.[row];
|
|
//if (current === undefined) return;
|
|
//todo: complete
|
|
state._events.dispatchEvent(
|
|
new CustomEvent('onCellEdited', {
|
|
detail: { cell, newVal, row, state },
|
|
})
|
|
);
|
|
},
|
|
onColumnMoved: (from: number, to: number) => {
|
|
const s = get();
|
|
const fromItem = s.renderColumns?.[from];
|
|
const toItem = s.renderColumns?.[to];
|
|
if (fromItem?.disableMove || toItem?.disableMove) {
|
|
return;
|
|
}
|
|
|
|
s.setStateFN('colOrder', (cols) => {
|
|
const renderCols =
|
|
cols ??
|
|
s.renderColumns
|
|
?.map((col, i) => [col.id, i])
|
|
.reduce((acc, [id, i]) => ({ ...acc, [id]: i }), {});
|
|
|
|
if (!fromItem?.id || !toItem?.id) {
|
|
return renderCols;
|
|
}
|
|
|
|
return { ...renderCols, [fromItem?.id]: to, [toItem?.id]: from };
|
|
});
|
|
},
|
|
onColumnProposeMove: (startIndex: number, endIndex: number) => {
|
|
const s = get();
|
|
const fromItem = s.renderColumns?.[startIndex];
|
|
const toItem = s.renderColumns?.[endIndex];
|
|
if (fromItem?.disableMove || toItem?.disableMove) {
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
onColumnResize: (
|
|
column: GridColumn,
|
|
newSize: number,
|
|
_colIndex: number,
|
|
_newSizeWithGrow: number
|
|
) => {
|
|
const s = get();
|
|
const col = s.renderColumns?.find((col) => col.id === column.id);
|
|
if (col?.disableResize) {
|
|
return;
|
|
}
|
|
if (col?.maxWidth && newSize < col?.maxWidth) {
|
|
return;
|
|
}
|
|
|
|
if (col) {
|
|
s.setStateFN('colSize', (cols) => {
|
|
if (cols && col && cols[col.id] === newSize) {
|
|
return cols;
|
|
}
|
|
return { ...cols, [col.id]: newSize };
|
|
});
|
|
}
|
|
},
|
|
|
|
onContextClick: (area: string, event: any, col?: number, row?: number) => {
|
|
const s = get();
|
|
const coldef = s.renderColumns?.[col ?? -1];
|
|
|
|
const items =
|
|
area === 'menu'
|
|
? [
|
|
{
|
|
label: `Side menu`,
|
|
},
|
|
]
|
|
: coldef
|
|
? [
|
|
{
|
|
items: [
|
|
{
|
|
label: 'Sort Ascending',
|
|
leftSection: <SpriteImage sprite={SortUpSprite} />,
|
|
onClick: () => {
|
|
s.setStateFN('colSort', (c) => {
|
|
const cols = [...(c ?? [])];
|
|
const idx = cols.findIndex((search) => search.id === coldef.id);
|
|
const dir = 'asc';
|
|
if (idx < 0) {
|
|
const newSort: SortOption = {
|
|
direction: dir,
|
|
id: coldef.id,
|
|
order: cols?.length,
|
|
};
|
|
cols.push(newSort);
|
|
} else if (idx >= 0) {
|
|
cols[idx].direction = dir;
|
|
}
|
|
|
|
return cols;
|
|
});
|
|
},
|
|
},
|
|
{
|
|
label: 'Sort Descending',
|
|
leftSection: <SpriteImage sprite={SortDownSprite} />,
|
|
onClick: () => {
|
|
s.setStateFN('colSort', (c) => {
|
|
const cols = [...(c ?? [])];
|
|
const idx = cols.findIndex((search) => search.id === coldef.id);
|
|
const dir = 'desc';
|
|
if (idx < 0) {
|
|
const newSort: SortOption = {
|
|
direction: dir,
|
|
id: coldef.id,
|
|
order: cols?.length,
|
|
};
|
|
cols.push(newSort);
|
|
} else if (idx >= 0) {
|
|
cols[idx].direction = dir;
|
|
}
|
|
|
|
return cols;
|
|
});
|
|
},
|
|
},
|
|
{
|
|
label: `Filter ${coldef?.title ?? coldef?.id}`,
|
|
},
|
|
{
|
|
renderer: <ColumnFilterSet column={coldef} storeState={get()} />,
|
|
},
|
|
],
|
|
label: `Column Settings for ${coldef?.title ?? coldef?.id}`,
|
|
leftSection: <IconGrid4x4 size={16} />,
|
|
},
|
|
]
|
|
: [];
|
|
s.hideMenu?.(area);
|
|
|
|
s.showMenu?.(area, {
|
|
items:
|
|
coldef?.getMenuItems?.(
|
|
area,
|
|
s,
|
|
col && row ? s.getRowBuffer(row) : undefined,
|
|
coldef,
|
|
items
|
|
) ??
|
|
s.getMenuItems?.(area, s, col && row ? s.getRowBuffer(row) : undefined, coldef, items) ??
|
|
items,
|
|
x: event.clientX ?? event.bounds?.x,
|
|
y: event.clientY ?? event.bounds?.y,
|
|
});
|
|
},
|
|
onHeaderClicked: (colIndex: number, event: HeaderClickedEventArgs) => {
|
|
const s = get();
|
|
event.preventDefault();
|
|
const col = s.renderColumns?.[colIndex];
|
|
if (!col) {
|
|
return;
|
|
}
|
|
if (!col.disableSort) {
|
|
s.setStateFN('colSort', (c) => {
|
|
const cols = [...(c ?? [])];
|
|
const idx = cols.findIndex((search) => search.id === col.id);
|
|
|
|
if (idx < 0) {
|
|
const newSort: SortOption = { direction: 'asc', id: col.id, order: cols?.length };
|
|
cols.push(newSort);
|
|
} else if (idx >= 0 && cols[idx].direction === 'asc') {
|
|
cols[idx].direction = 'desc';
|
|
} else if (idx >= 0 && cols[idx].direction === 'desc') {
|
|
cols.splice(idx, 1);
|
|
}
|
|
|
|
return cols;
|
|
});
|
|
}
|
|
},
|
|
onHeaderMenuClick: (col: number, screenPosition: Rectangle) => {
|
|
const s = get();
|
|
const coldef = s.renderColumns?.[col];
|
|
if (!coldef) {
|
|
return;
|
|
}
|
|
|
|
const sortItems = [
|
|
{
|
|
label: `Sort ${coldef?.title ?? coldef?.id}`,
|
|
},
|
|
{
|
|
label: 'Sort Ascending',
|
|
leftSection: <SpriteImage sprite={SortUpSprite} />,
|
|
onClick: () => {
|
|
s.setStateFN('colSort', (c) => {
|
|
const cols = [...(c ?? [])];
|
|
const idx = cols.findIndex((search) => search.id === coldef.id);
|
|
const dir = 'asc';
|
|
if (idx < 0) {
|
|
const newSort: SortOption = { direction: dir, id: coldef.id, order: cols?.length };
|
|
cols.push(newSort);
|
|
} else if (idx >= 0) {
|
|
cols[idx].direction = dir;
|
|
}
|
|
|
|
return cols;
|
|
});
|
|
},
|
|
},
|
|
{
|
|
label: 'Sort Descending',
|
|
leftSection: <SpriteImage sprite={SortDownSprite} />,
|
|
onClick: () => {
|
|
s.setStateFN('colSort', (c) => {
|
|
const cols = [...(c ?? [])];
|
|
const idx = cols.findIndex((search) => search.id === coldef.id);
|
|
const dir = 'desc';
|
|
if (idx < 0) {
|
|
const newSort: SortOption = { direction: dir, id: coldef.id, order: cols?.length };
|
|
cols.push(newSort);
|
|
} else if (idx >= 0) {
|
|
cols[idx].direction = dir;
|
|
}
|
|
|
|
return cols;
|
|
});
|
|
},
|
|
},
|
|
{
|
|
isDivider: true,
|
|
},
|
|
];
|
|
const items = [
|
|
...(coldef.disableSort ? [] : sortItems),
|
|
{
|
|
label: `Filter ${coldef?.title ?? coldef?.id}`,
|
|
},
|
|
{
|
|
renderer: <ColumnFilterSet column={coldef} storeState={get()} />,
|
|
},
|
|
{
|
|
isDivider: true,
|
|
},
|
|
{
|
|
label: `Refresh`,
|
|
onClickAsync: async () => {
|
|
await s.reload?.();
|
|
},
|
|
},
|
|
];
|
|
s.hideMenu?.('header-menu');
|
|
s.showMenu?.('header-menu', {
|
|
items:
|
|
coldef?.getMenuItems?.('header-menu', s, undefined, coldef, items) ??
|
|
s.getMenuItems?.('header-menu', s, undefined, coldef, items),
|
|
x: screenPosition.x,
|
|
y: screenPosition.y,
|
|
});
|
|
},
|
|
onItemHovered: (args: GridMouseEventArgs) => {
|
|
const s = get();
|
|
s.setState('_activeTooltip', undefined);
|
|
if (args.kind === 'cell') {
|
|
//_activeTooltip
|
|
const coldef = s.renderColumns?.[args.location[0]];
|
|
if (coldef?.tooltip && typeof coldef?.tooltip === 'string') {
|
|
s.setState('_activeTooltip', coldef?.tooltip);
|
|
} else if (coldef?.tooltip && typeof coldef?.tooltip === 'function') {
|
|
const buffer = s.getRowBuffer(args.location[1]);
|
|
s.setState('_activeTooltip', coldef?.tooltip(buffer, args.location[1], args.location[0]));
|
|
}
|
|
}
|
|
},
|
|
onVisibleRegionChanged: (
|
|
region: Rectangle,
|
|
_tx: number,
|
|
_ty: number,
|
|
_extras: {
|
|
freezeRegion?: Rectangle;
|
|
freezeRegions?: readonly Rectangle[];
|
|
selected?: Item;
|
|
}
|
|
) => {
|
|
const state = get();
|
|
if (state._scrollTimeout) {
|
|
clearTimeout(state._scrollTimeout);
|
|
}
|
|
|
|
const onVisibleRegionChangedDebounced = () => {
|
|
//console.log('Gridler:Debug:VisibleRegionChanged', r);
|
|
const state = get();
|
|
const firstPage = Math.max(0, Math.floor(region.y / state.pageSize));
|
|
// const lastPage = Math.floor((region.y + region.height) / state.pageSize);
|
|
// const upperPage = state.pageSize * firstPage;
|
|
|
|
const previousPage = Math.max(0, Math.floor(state._visiblePages.y / state.pageSize));
|
|
const pageDif = firstPage - previousPage;
|
|
// console.log(
|
|
// 'Page dif',
|
|
// pageDif,
|
|
// { previousPage, firstPage, lastPage, onder: pageDif - 1 === lastPage },
|
|
// region
|
|
// );
|
|
// if (pageDif - 1 === lastPage) {
|
|
// console.log('Is onder', lastPage, pageDif);
|
|
// }
|
|
|
|
if (state.progressiveScroll && pageDif > 1) {
|
|
const state = get();
|
|
const newpos = (previousPage + 2) * state.pageSize * (state.rowHeight ?? 22);
|
|
// console.log('Pending scroll exec', newpos, {
|
|
// pageSize: state.pageSize,
|
|
// rowHeight: state.rowHeight
|
|
// });
|
|
state._glideref?.scrollTo(
|
|
0,
|
|
{
|
|
amount: newpos,
|
|
unit: 'px',
|
|
},
|
|
'vertical'
|
|
);
|
|
|
|
return;
|
|
}
|
|
//console.log('VIsi change', { firstPage, lastPage }, state?._page_data[lastPage]);
|
|
// if (state._active_requests && state._active_requests.filter((f)=>f.page > firstPage - 1)) {
|
|
|
|
// }
|
|
|
|
//console.log('Gridler:Debug:VisibleRegionChanged', region, firstPage, lastPage);
|
|
state.setState('_visiblePages', region);
|
|
};
|
|
|
|
state.setState(
|
|
'_scrollTimeout',
|
|
setTimeout(() => {
|
|
onVisibleRegionChangedDebounced();
|
|
}, 100)
|
|
);
|
|
},
|
|
|
|
pageSize: 50,
|
|
setState: (key, value) => {
|
|
set(
|
|
produce((state) => {
|
|
state[key] = value;
|
|
})
|
|
);
|
|
},
|
|
setStateFN: (key, value) => {
|
|
const p = new Promise<void>((resolve, reject) => {
|
|
set(
|
|
produce((state) => {
|
|
if (typeof value === 'function') {
|
|
state[key] = (value as (value: any) => any)(state[key]);
|
|
} else {
|
|
reject(new Error(`Not a function ${value}`));
|
|
throw Error(`Not a function ${value}`);
|
|
}
|
|
})
|
|
);
|
|
resolve();
|
|
});
|
|
|
|
return p;
|
|
},
|
|
toCell: <T extends Record<string, string>>(row: T, col: number): GridCell => {
|
|
const s = get();
|
|
const columns = s.renderColumns;
|
|
const coldef: GridlerColumn | undefined = columns?.[col];
|
|
const ref: string = coldef?.id ?? coldef?.title ?? String(col);
|
|
|
|
if (ref === undefined || ref === null || row === undefined || row === null) {
|
|
if (coldef?.Cell) {
|
|
return coldef?.Cell(row, col, ref, undefined, s) as GridCell;
|
|
}
|
|
if (s.RenderCell) {
|
|
return s.RenderCell(row, col, ref, undefined, s);
|
|
}
|
|
|
|
return {
|
|
allowOverlay: false,
|
|
kind: GridCellKind.Loading,
|
|
};
|
|
}
|
|
|
|
try {
|
|
const val = String(ref).includes('.') ? (getNestedValue(ref, row) ?? '') : row?.[ref];
|
|
|
|
if (coldef?.Cell) {
|
|
return coldef?.Cell(row, col, ref, val, s) as GridCell;
|
|
}
|
|
if (s.RenderCell) {
|
|
return s.RenderCell(row, col, ref, val, s);
|
|
}
|
|
|
|
return {
|
|
allowOverlay: true,
|
|
data: val,
|
|
displayData: String(val),
|
|
kind: GridCellKind.Text,
|
|
};
|
|
} catch (e) {
|
|
if (s.RenderCell) {
|
|
return s.RenderCell(row, col, ref, row?.[ref ?? ''], s);
|
|
}
|
|
|
|
return {
|
|
allowOverlay: false,
|
|
kind: GridCellKind.Loading,
|
|
skeletonWidthVariability: 50,
|
|
};
|
|
}
|
|
},
|
|
total_rows: 1000,
|
|
uniqueid: getUUID(),
|
|
}),
|
|
(props) => {
|
|
const [setState, getState] = props.useStore((s) => [s.setState, s.getState]);
|
|
|
|
const menus = useMantineBetterMenus();
|
|
|
|
useEffect(() => {
|
|
const onMounted = getState('onMounted');
|
|
if (typeof onMounted === 'function') {
|
|
onMounted(getState, setState);
|
|
}
|
|
setState('mounted', true);
|
|
if (window && window.document) {
|
|
const portalElement = window.document.getElementById('portal');
|
|
if (!portalElement) {
|
|
const div = window.document.createElement('div');
|
|
div.id = 'portal';
|
|
div.setAttribute('data-gridler-portal', props.uniqueid);
|
|
window.document.body.appendChild(div);
|
|
}
|
|
}
|
|
getState('_events').dispatchEvent(
|
|
new CustomEvent('mounted', {
|
|
detail: {},
|
|
})
|
|
);
|
|
return () => {
|
|
const onUnMounted = getState('onUnMounted');
|
|
setState('mounted', false);
|
|
getState('_events').dispatchEvent(
|
|
new CustomEvent('unmounted', {
|
|
detail: {},
|
|
})
|
|
);
|
|
if (typeof onUnMounted === 'function') {
|
|
onUnMounted();
|
|
}
|
|
};
|
|
}, [setState, getState]);
|
|
|
|
/// 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);
|
|
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')?.();
|
|
});
|
|
|
|
return {
|
|
...props,
|
|
|
|
hideMenu: props.hideMenu ?? menus.hide,
|
|
showMenu: props.showMenu ?? menus.show,
|
|
total_rows: props.total_rows ?? getState('total_rows') ?? 0,
|
|
};
|
|
}
|
|
);
|
|
|
|
export type useStoreReturnType = ReturnType<typeof useGridlerStore>;
|
|
|
|
export { Provider, useGridlerStore };
|