diff --git a/src/Gridler/GridlerDataGrid.tsx b/src/Gridler/GridlerDataGrid.tsx index 6f1b126..148d8db 100644 --- a/src/Gridler/GridlerDataGrid.tsx +++ b/src/Gridler/GridlerDataGrid.tsx @@ -5,7 +5,7 @@ import { type DataEditorRef, type GridColumn, } from '@glideapps/glide-data-grid'; -import { ActionIcon, Stack } from '@mantine/core'; +import { ActionIcon, Group, Stack } from '@mantine/core'; import { useElementSize, useMergedRef } from '@mantine/hooks'; import { IconMenu2 } from '@tabler/icons-react'; import React from 'react'; @@ -19,6 +19,7 @@ import { SortUpSprite } from './components/sprites/SortUp'; import { useGridlerStore } from './components/Store'; import classes from './Gridler.module.css'; import { useGridTheme } from './hooks/use-grid-theme'; +import { RightMenuIcon } from './components/RightMenuIcon'; export const GridlerDataGrid = () => { const ref = React.useRef(null); @@ -195,14 +196,11 @@ export const GridlerDataGrid = () => { rangeSelect="multi-rect" ref={refMerged as any} rightElement={ - onContextClick('menu', e)} - variant="subtle" - > - - + + {sections?.rightElementStart} + + {sections?.rightElementEnd} + } rowHeight={rowHeight ?? 22} //rowMarkersCheckboxStyle='square' diff --git a/src/Gridler/components/APIAdaptorGoLangv2.tsx b/src/Gridler/components/APIAdaptorGoLangv2.tsx index 09aacaa..f09b779 100644 --- a/src/Gridler/components/APIAdaptorGoLangv2.tsx +++ b/src/Gridler/components/APIAdaptorGoLangv2.tsx @@ -20,70 +20,80 @@ 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)); - head.set('x-offset', String((pageSize ?? 50) * index)); + setState('loadingData', true); + try { + //console.log('APIAdaptorGoLangv2', { _active_requests, index, pageSize, props }); + if (props && props.url) { + const head = new Headers(); + head.set('x-limit', String(pageSize ?? 50)); + head.set('x-offset', String((pageSize ?? 50) * index)); - head.set('Authorization', `Token ${props.authtoken}`); + head.set('Authorization', `Token ${props.authtoken}`); - if (colSort?.length && colSort.length > 0) { - head.set( - 'x-sort', - colSort - ?.map((sort: any) => `${sort.id} ${sort.direction}`) - .reduce((acc: any, val: any) => `${acc},${val}`) + if (colSort?.length && colSort.length > 0) { + head.set( + 'x-sort', + colSort + ?.map((sort: any) => `${sort.id} ${sort.direction}`) + .reduce((acc: any, val: any) => `${acc},${val}`) + ); + } + + 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 currentRequestIndex = _active_requests?.findIndex((f) => f.page === index) ?? -1; + _active_requests?.forEach((r) => { + if ((r.page >= 0 && r.page < index - 2) || (index >= 0 && r.page > index + 2)) { + r.controller?.abort?.(); + } + }); + if (_active_requests && currentRequestIndex >= 0 && _active_requests[currentRequestIndex]) { + //console.log(`Already queued ${index}`, index, s._active_requests); + setState('loadingData', false); + return undefined; + } + + const controller = new AbortController(); + 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, + } ); - } - 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}`); - } - }); - } + if (res.ok) { + const cr = res.headers.get('Content-Range')?.split('/'); + if (cr?.[1] && parseInt(cr[1], 10) > 0) { + setState('total_rows', parseInt(cr[1], 10)); + } - const currentRequestIndex = _active_requests?.findIndex((f) => f.page === index) ?? -1; - _active_requests?.forEach((r) => { - if ((r.page >= 0 && r.page < index - 2) || (index >= 0 && r.page > index + 2)) { - r.controller?.abort?.(); + const data = await res.json(); + setState('loadingData', false); + return data ?? []; } - }); - if (_active_requests && currentRequestIndex >= 0 && _active_requests[currentRequestIndex]) { - //console.log(`Already queued ${index}`, index, s._active_requests); - return undefined; + addError(`${res.status} ${res.statusText}`, 'api', props.url); + + await setStateFN('_active_requests', (cv) => [ + ...(cv ?? []).filter((f) => f.page !== index), + ]); } - - const controller = new AbortController(); - 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, - } - ); - - if (res.ok) { - const cr = res.headers.get('Content-Range')?.split('/'); - if (cr?.[1] && parseInt(cr[1], 10) > 0) { - setState('total_rows', parseInt(cr[1], 10)); - } - - const data = await res.json(); - - return data ?? []; - } - addError(`${res.status} ${res.statusText}`, 'api', props.url); - - await setStateFN('_active_requests', (cv) => [...(cv ?? []).filter((f) => f.page !== index)]); + } catch (e) { + //console.log('APIAdaptorGoLangv2 error', e); + addError(`Error: ${e}`, 'api', props.url); } + setState('loadingData', false); return []; }; diff --git a/src/Gridler/components/GridlerFormInterface.tsx b/src/Gridler/components/GridlerFormInterface.tsx new file mode 100644 index 0000000..9ccb605 --- /dev/null +++ b/src/Gridler/components/GridlerFormInterface.tsx @@ -0,0 +1,26 @@ +import { useEffect } from 'react'; + +import type { FormRequestType } from '../utils/types'; + +import { type GridlerProps, useGridlerStore } from './Store'; + +export function GlidlerFormInterface(props: { + getMenuItems?: GridlerProps['getMenuItems']; + onRequestForm: (request: FormRequestType, data: Record) => void; +}) { + // const [getMenuItems, getState, , mounted, setState] = useGridlerStore((s) => [ + // s.getMenuItems, + // s.getState, + // s.mounted, + // s.setState, + // ]); + + // useEffect(() => { + // if (mounted) { + // setState('getMenuItems', props.getMenuItems); + // setState('onRequestForm', props.onRequestForm); + // } + // }, [props.getMenuItems, props.onRequestForm, mounted, setState]); + + return <>; +} diff --git a/src/Gridler/components/RightMenuIcon.tsx b/src/Gridler/components/RightMenuIcon.tsx new file mode 100644 index 0000000..4664f4e --- /dev/null +++ b/src/Gridler/components/RightMenuIcon.tsx @@ -0,0 +1,23 @@ +import { ActionIcon } from '@mantine/core'; +import { IconMenu2 } from '@tabler/icons-react'; + +import { useGridlerStore } from './Store'; + +export function RightMenuIcon() { + const { loadingData, onContextClick } = useGridlerStore((s) => ({ + loadingData: s.loadingData, + onContextClick: s.onContextClick, + })); + + return ( + onContextClick('menu', e)} + variant="subtle" + > + + + ); +} diff --git a/src/Gridler/components/Store.tsx b/src/Gridler/components/Store.tsx index b08e28c..b265781 100644 --- a/src/Gridler/components/Store.tsx +++ b/src/Gridler/components/Store.tsx @@ -23,7 +23,7 @@ import { produce } from 'immer'; import { type PropsWithChildren, type ReactNode, useEffect } from 'react'; import { type MantineBetterMenuInstance, useMantineBetterMenus } from '../../MantineBetterMenu'; -import { type TRequest } from '../utils/types'; +import { type FormRequestType } from '../utils/types'; import { ColumnFilterSet, type GridlerColumn, type GridlerColumns } from './Column'; import { SortDownSprite } from './sprites/SortDown'; import { SortUpSprite } from './sprites/SortUp'; @@ -80,12 +80,14 @@ export interface GridlerProps extends PropsWithChildren { value: any, store: GridlerState ) => GridCell; - request?: TRequest; + rowHeight?: number; sections?: { bottom?: React.ReactNode; left?: React.ReactNode; right?: React.ReactNode; + rightElementEnd?: React.ReactNode; + rightElementStart?: React.ReactNode; top?: React.ReactNode; }; selectedRow?: number; @@ -115,8 +117,8 @@ export interface GridlerState { colSize?: Record; colSort?: Array; data?: Array; - errors: Array; + focused?: boolean; get: () => GridlerState; getCellContent: (cell: Item) => GridCell; @@ -126,8 +128,9 @@ export interface GridlerState { ) => CellArray | GetCellsThunk; getRowBuffer: (row: number) => any; getState: (key: K) => GridlerStoreState[K]; - hasLocalData: boolean; + + loadingData?: boolean; loadPage: (page: number, clearMode?: 'all' | 'page') => Promise; mounted: boolean; onCellEdited: (cell: Item, newVal: EditableGridCell) => void; diff --git a/src/Gridler/stories/Examples.goapi.tsx b/src/Gridler/stories/Examples.goapi.tsx index 7d3cdf6..73fa09d 100644 --- a/src/Gridler/stories/Examples.goapi.tsx +++ b/src/Gridler/stories/Examples.goapi.tsx @@ -1,4 +1,4 @@ -import { Divider, Group, Stack, TagsInput, TextInput } from '@mantine/core'; +import { Checkbox, Divider, Group, Stack, TagsInput, TextInput } from '@mantine/core'; import { useLocalStorage } from '@mantine/hooks'; import { useState } from 'react'; @@ -15,6 +15,7 @@ export const GridlerGoAPIExampleEventlog = () => { const [apiKey, setApiKey] = useLocalStorage({ defaultValue: '', key: 'apikey' }); const [selectRow, setSelectRow] = useState(''); const [values, setValues] = useState>>([]); + const [sections, setSections] = useState | undefined>(undefined); const columns: GridlerColumns = [ { id: 'id_process', @@ -49,6 +50,21 @@ export const GridlerGoAPIExampleEventlog = () => { setApiUrl(e.target.value)} value={apiUrl} /> setApiKey(e.target.value)} value={apiKey} /> + { + e.target.checked + ? setSections({ + bottom:
bottom
, + left:
L
, + right:
R
, + top:
top
, + }) + : setSections(undefined); + }} + /> + { //console.log('GridlerGoAPIExampleEventlog onChange', v); setValues(v); }} - sections={{ - bottom:
bottom
, - left:
L
, - right:
R
, - top:
top
, - }} + sections={sections} selectedRow={selectRow ? parseInt(selectRow, 10) : undefined} selectMode="row" uniqueid="gridtest" diff --git a/src/Gridler/utils/types.ts b/src/Gridler/utils/types.ts index 223e57d..772ec7a 100644 --- a/src/Gridler/utils/types.ts +++ b/src/Gridler/utils/types.ts @@ -15,4 +15,4 @@ export interface APIOptions { url?: string; } -export type TRequest = 'change' | 'delete' | 'insert' | 'select' ; +export type FormRequestType = 'change' | 'delete' | 'insert' | 'select' ;