A lot of refectoring

This commit is contained in:
Hein 2025-10-21 14:10:59 +02:00
parent 4186219c50
commit c92cabc569
24 changed files with 756 additions and 626 deletions

View File

@ -48,9 +48,9 @@
}, },
"dependencies": { "dependencies": {
"@glideapps/glide-data-grid": "^6.0.3", "@glideapps/glide-data-grid": "^6.0.3",
"@mantine/notifications": "^8.3.1", "@mantine/notifications": "^8.3.5",
"@tabler/icons-react": "^3.35.0", "@tabler/icons-react": "^3.35.0",
"@tanstack/react-query": "^5.89.0", "@tanstack/react-query": "^5.90.5",
"immer": "^10.1.3", "immer": "^10.1.3",
"moment": "^2.30.1", "moment": "^2.30.1",
"postcss": "^8.5.6", "postcss": "^8.5.6",
@ -59,32 +59,33 @@
}, },
"devDependencies": { "devDependencies": {
"@changesets/cli": "^2.29.7", "@changesets/cli": "^2.29.7",
"@eslint/js": "^9.35.0", "@eslint/js": "^9.38.0",
"@storybook/react-vite": "^9.1.7", "@storybook/react-vite": "^9.1.13",
"@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1", "@testing-library/user-event": "^14.6.1",
"@types/node": "^24.4.0", "@types/node": "^24.9.1",
"@types/react": "^19.1.13", "@types/react": "^19.2.2",
"@types/react-dom": "^19.1.9", "@types/react-dom": "^19.2.2",
"@typescript-eslint/parser": "^8.43.0", "@typescript-eslint/parser": "^8.46.2",
"@vitejs/plugin-react-swc": "^4.0.1", "@vitejs/plugin-react-swc": "^4.1.0",
"eslint": "^9.35.0", "eslint": "^9.38.0",
"eslint-config-mantine": "^4.0.3", "eslint-config-mantine": "^4.0.3",
"eslint-plugin-perfectionist": "^4.15.0", "eslint-plugin-perfectionist": "^4.15.1",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^7.0.0",
"eslint-plugin-react-refresh": "^0.4.20", "eslint-plugin-react-refresh": "^0.4.24",
"eslint-plugin-storybook": "^9.1.7", "eslint-plugin-storybook": "^9.1.13",
"global": "^4.4.0", "global": "^4.4.0",
"globals": "^16.4.0", "globals": "^16.4.0",
"jsdom": "^27.0.0", "jsdom": "^27.0.1",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"prettier-eslint": "^16.4.2", "prettier-eslint": "^16.4.2",
"react-dom": "^19.1.1", "react": "^19.2.0",
"storybook": "^9.1.7", "react-dom": "^19.2.0",
"typescript": "~5.9.2", "storybook": "^9.1.13",
"typescript-eslint": "^8.43.0", "typescript": "~5.9.3",
"vite": "^7.1.5", "typescript-eslint": "^8.46.2",
"vite": "^7.1.11",
"vite-plugin-dts": "^4.5.4", "vite-plugin-dts": "^4.5.4",
"vite-tsconfig-paths": "^5.1.4", "vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.2.4" "vitest": "^3.2.4"
@ -95,6 +96,7 @@
"@warkypublic/artemis-kit": "^1.0.10", "@warkypublic/artemis-kit": "^1.0.10",
"@warkypublic/zustandsyncstore": "^0.0.4", "@warkypublic/zustandsyncstore": "^0.0.4",
"react": ">= 19.0.0", "react": ">= 19.0.0",
"react-dom": ">= 19.0.0",
"use-sync-external-store": ">= 1.4.0", "use-sync-external-store": ">= 1.4.0",
"zustand": ">= 5.0.0" "zustand": ">= 5.0.0"
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,71 +1,73 @@
import { MantineProvider } from '@mantine/core' import { MantineProvider } from '@mantine/core';
import { render, screen } from '@testing-library/react' import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest' import { describe, expect, it, vi } from 'vitest';
import { Gridler } from './Gridler' import { Gridler } from './Gridler';
// Mock the complex sub-components // Mock the complex sub-components
vi.mock('./GridlerDataGrid', () => ({ vi.mock('./GridlerDataGrid', () => ({
GridlerDataGrid: () => <div data-testid="gridler-data-grid">Data Grid</div> GridlerDataGrid: () => <div data-testid="gridler-data-grid">Data Grid</div>,
})) }));
vi.mock('../MantineBetterMenu', () => ({ vi.mock('../MantineBetterMenu', () => ({
MantineBetterMenusProvider: ({ children }: { children: React.ReactNode }) => ( MantineBetterMenusProvider: ({ children }: { children: React.ReactNode }) => (
<div data-testid="menu-provider">{children}</div> <div data-testid="menu-provider">{children}</div>
) ),
})) useMantineBetterMenus: () => ({
hide: vi.fn(),
show: vi.fn(),
}),
}));
// Mock the Store Provider // Mock the Store Provider
vi.mock('./components/Store', () => ({ vi.mock('./components/GridlerStore', () => ({
Provider: ({ children, uniqueid }: { children: React.ReactNode; uniqueid: string }) => ( Provider: ({ children, uniqueid }: { children: React.ReactNode; uniqueid: string }) => (
<div data-testid={`store-provider-${uniqueid}`}>{children}</div> <div data-testid={`store-provider-${uniqueid}`}>{children}</div>
) ),
})) }));
const TestWrapper = ({ children }: { children: React.ReactNode }) => ( const TestWrapper = ({ children }: { children: React.ReactNode }) => (
<MantineProvider> <MantineProvider>{children}</MantineProvider>
{children} );
</MantineProvider>
)
describe('Gridler', () => { describe('Gridler', () => {
const defaultProps = { const defaultProps = {
cols: [ cols: [
{ key: 'id', title: 'ID' }, { key: 'id', title: 'ID' },
{ key: 'name', title: 'Name' } { key: 'name', title: 'Name' },
], ],
uniqueid: 'test-grid' uniqueid: 'test-grid',
} };
it('renders without crashing', () => { it('renders without crashing', () => {
render( render(
<TestWrapper> <TestWrapper>
<Gridler {...defaultProps} /> <Gridler {...defaultProps} />
</TestWrapper> </TestWrapper>
) );
expect(screen.getByTestId('gridler-data-grid')).toBeInTheDocument() expect(screen.getByTestId('gridler-data-grid')).toBeInTheDocument();
}) });
it('wraps content with MantineBetterMenusProvider', () => { it('wraps content with MantineBetterMenusProvider', () => {
render( render(
<TestWrapper> <TestWrapper>
<Gridler {...defaultProps} /> <Gridler {...defaultProps} />
</TestWrapper> </TestWrapper>
) );
expect(screen.getByTestId('menu-provider')).toBeInTheDocument() expect(screen.getByTestId('menu-provider')).toBeInTheDocument();
}) });
it('creates store provider with unique ID', () => { it('creates store provider with unique ID', () => {
render( render(
<TestWrapper> <TestWrapper>
<Gridler {...defaultProps} /> <Gridler {...defaultProps} />
</TestWrapper> </TestWrapper>
) );
expect(screen.getByTestId('store-provider-test-grid')).toBeInTheDocument() expect(screen.getByTestId('store-provider-test-grid')).toBeInTheDocument();
}) });
it('renders children when provided', () => { it('renders children when provided', () => {
render( render(
@ -74,18 +76,18 @@ describe('Gridler', () => {
<div data-testid="test-child">Custom Child</div> <div data-testid="test-child">Custom Child</div>
</Gridler> </Gridler>
</TestWrapper> </TestWrapper>
) );
expect(screen.getByTestId('test-child')).toBeInTheDocument() expect(screen.getByTestId('test-child')).toBeInTheDocument();
}) });
it('handles different uniqueid prop', () => { it('handles different uniqueid prop', () => {
render( render(
<TestWrapper> <TestWrapper>
<Gridler {...defaultProps} uniqueid="different-grid" /> <Gridler {...defaultProps} uniqueid="different-grid" />
</TestWrapper> </TestWrapper>
) );
expect(screen.getByTestId('store-provider-different-grid')).toBeInTheDocument() expect(screen.getByTestId('store-provider-different-grid')).toBeInTheDocument();
}) });
}) });

View File

@ -2,13 +2,12 @@ import '@glideapps/glide-data-grid/dist/index.css';
import React from 'react'; import React from 'react';
import { MantineBetterMenusProvider } from '../MantineBetterMenu'; import { MantineBetterMenusProvider } from '../MantineBetterMenu';
import { APIAdaptorGoLangv2 } from './components/APIAdaptorGoLangv2'; import { GlidlerFormAdaptor } from './components/adaptors/GlidlerFormAdaptor';
import { GlidlerFormInterface } from './components/GridlerFormInterface'; import { GlidlerLocalDataAdaptor } from './components/adaptors/GlidlerLocalDataAdaptor';
import { LocalDataAdaptor } from './components/LocalDataAdaptor'; import { type GridlerProps, Provider } from './components/GridlerStore';
import { type GridlerProps, Provider } from './components/Store';
import { GridlerDataGrid } from './GridlerDataGrid'; import { GridlerDataGrid } from './GridlerDataGrid';
export const Gridler = (props: GridlerProps) => { const Gridler = (props: GridlerProps) => {
return ( return (
<MantineBetterMenusProvider> <MantineBetterMenusProvider>
<Provider <Provider
@ -27,6 +26,8 @@ export const Gridler = (props: GridlerProps) => {
); );
}; };
Gridler.GlidlerFormInterface = GlidlerFormInterface; Gridler.FormAdaptor = GlidlerFormAdaptor;
Gridler.APIAdaptorGoLangv2 = APIAdaptorGoLangv2; Gridler.LocalDataAdaptor = GlidlerLocalDataAdaptor;
Gridler.LocalDataAdaptor = LocalDataAdaptor;
export { Gridler };
export default Gridler;

View File

@ -5,21 +5,20 @@ import {
type DataEditorRef, type DataEditorRef,
type GridColumn, type GridColumn,
} from '@glideapps/glide-data-grid'; } from '@glideapps/glide-data-grid';
import { ActionIcon, Group, Stack } from '@mantine/core'; import { Group, Stack } from '@mantine/core';
import { useElementSize, useMergedRef } from '@mantine/hooks'; import { useMergedRef } from '@mantine/hooks';
import { IconMenu2 } from '@tabler/icons-react';
import React from 'react'; import React from 'react';
import { BottomBar } from './components/BottomBar'; import { BottomBar } from './components/BottomBar';
import { Computer } from './components/Computer'; import { Computer } from './components/Computer';
import { useGridlerStore } from './components/GridlerStore';
import { Pager } from './components/Pager'; import { Pager } from './components/Pager';
import { RightMenuIcon } from './components/RightMenuIcon';
import { SortSprite } from './components/sprites/Sort'; import { SortSprite } from './components/sprites/Sort';
import { SortDownSprite } from './components/sprites/SortDown'; import { SortDownSprite } from './components/sprites/SortDown';
import { SortUpSprite } from './components/sprites/SortUp'; import { SortUpSprite } from './components/sprites/SortUp';
import { useGridlerStore } from './components/Store';
import classes from './Gridler.module.css'; import classes from './Gridler.module.css';
import { useGridTheme } from './hooks/use-grid-theme'; import { useGridTheme } from './hooks/use-grid-theme';
import { RightMenuIcon } from './components/RightMenuIcon';
export const GridlerDataGrid = () => { export const GridlerDataGrid = () => {
const ref = React.useRef<DataEditorRef | null>(null); const ref = React.useRef<DataEditorRef | null>(null);
@ -34,6 +33,7 @@ export const GridlerDataGrid = () => {
glideProps, glideProps,
hasLocalData, hasLocalData,
headerHeight, headerHeight,
heightProp,
mounted, mounted,
onCellEdited, onCellEdited,
onColumnMoved, onColumnMoved,
@ -51,7 +51,6 @@ export const GridlerDataGrid = () => {
setState, setState,
setStateFN, setStateFN,
total_rows, total_rows,
heightProp,
widthProp, widthProp,
} = useGridlerStore((s) => ({ } = useGridlerStore((s) => ({
_gridSelection: s._gridSelection, _gridSelection: s._gridSelection,
@ -62,6 +61,7 @@ export const GridlerDataGrid = () => {
glideProps: s.glideProps, glideProps: s.glideProps,
hasLocalData: s.hasLocalData, hasLocalData: s.hasLocalData,
headerHeight: s.headerHeight, headerHeight: s.headerHeight,
heightProp: s.height,
mounted: s.mounted, mounted: s.mounted,
onCellEdited: s.onCellEdited, onCellEdited: s.onCellEdited,
onColumnMoved: s.onColumnMoved, onColumnMoved: s.onColumnMoved,
@ -79,7 +79,6 @@ export const GridlerDataGrid = () => {
setState: s.setState, setState: s.setState,
setStateFN: s.setStateFN, setStateFN: s.setStateFN,
total_rows: s.total_rows, total_rows: s.total_rows,
heightProp: s.height,
widthProp: s.width, widthProp: s.width,
})); }));
@ -209,7 +208,7 @@ export const GridlerDataGrid = () => {
checkboxStyle: 'square', checkboxStyle: 'square',
kind: 'both', kind: 'both',
}} }}
rows={total_rows} rows={total_rows ?? 0}
rowSelect="multi" rowSelect="multi"
rowSelectionMode="auto" rowSelectionMode="auto"
spanRangeBehavior="default" spanRangeBehavior="default"

View File

@ -1,4 +1,4 @@
import { useGridlerStore } from './Store'; import { useGridlerStore } from './GridlerStore';
export function BottomBar() { export function BottomBar() {
const { _activeTooltip, tooltipBarProps } = useGridlerStore((s) => ({ const { _activeTooltip, tooltipBarProps } = useGridlerStore((s) => ({

View File

@ -5,7 +5,7 @@ import { useDebouncedValue } from '@mantine/hooks';
import { IconX } from '@tabler/icons-react'; import { IconX } from '@tabler/icons-react';
import { type ReactNode, useEffect, useState } from 'react'; import { type ReactNode, useEffect, useState } from 'react';
import type { FilterOption, FilterOptionOperator, GridlerStoreState } from './Store'; import type { FilterOption, FilterOptionOperator, GridlerStoreState } from './GridlerStore';
export type GridCellLoose = { export type GridCellLoose = {
kind: GridCellKind | string; kind: GridCellKind | string;

View File

@ -2,7 +2,7 @@
import { CompactSelection } from '@glideapps/glide-data-grid'; import { CompactSelection } from '@glideapps/glide-data-grid';
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import { useGridlerStore } from './Store'; import { useGridlerStore } from './GridlerStore';
//The computer component does not need to be recalculated on every render, so we use React.memo to prevent unnecessary re-renders. //The computer component does not need to be recalculated on every render, so we use React.memo to prevent unnecessary re-renders.
export const Computer = React.memo(() => { export const Computer = React.memo(() => {

View File

@ -16,6 +16,7 @@ import {
type Item, type Item,
type Rectangle, type Rectangle,
} from '@glideapps/glide-data-grid'; } from '@glideapps/glide-data-grid';
import { IconGrid4x4 } from '@tabler/icons-react';
import { getUUID } from '@warkypublic/artemis-kit'; import { getUUID } from '@warkypublic/artemis-kit';
import { getNestedValue } from '@warkypublic/artemis-kit/object'; import { getNestedValue } from '@warkypublic/artemis-kit/object';
import { createSyncStore } from '@warkypublic/zustandsyncstore'; import { createSyncStore } from '@warkypublic/zustandsyncstore';
@ -27,12 +28,10 @@ import {
type MantineBetterMenuInstanceItem, type MantineBetterMenuInstanceItem,
useMantineBetterMenus, useMantineBetterMenus,
} from '../../MantineBetterMenu'; } from '../../MantineBetterMenu';
import { type FormRequestType } from '../utils/types';
import { ColumnFilterSet, type GridlerColumn, type GridlerColumns } from './Column'; import { ColumnFilterSet, type GridlerColumn, type GridlerColumns } from './Column';
import { SortDownSprite } from './sprites/SortDown'; import { SortDownSprite } from './sprites/SortDown';
import { SortUpSprite } from './sprites/SortUp'; import { SortUpSprite } from './sprites/SortUp';
import { SpriteImage } from './sprites/SpriteImage'; import { SpriteImage } from './sprites/SpriteImage';
import { IconGrid4x4 } from '@tabler/icons-react';
export type FilterOption = { export type FilterOption = {
datatype?: 'array' | 'boolean' | 'date' | 'function' | 'number' | 'object' | 'string'; datatype?: 'array' | 'boolean' | 'date' | 'function' | 'number' | 'object' | 'string';
@ -99,6 +98,7 @@ export interface GridlerProps extends PropsWithChildren {
selectMode?: 'cell' | 'row'; selectMode?: 'cell' | 'row';
showMenu?: (id: string, options?: Partial<MantineBetterMenuInstance>) => void; showMenu?: (id: string, options?: Partial<MantineBetterMenuInstance>) => void;
tooltipBarProps?: React.HTMLAttributes<HTMLDivElement>; tooltipBarProps?: React.HTMLAttributes<HTMLDivElement>;
total_rows?: number;
uniqueid: string; uniqueid: string;
useAPIQuery?: (index: number) => Promise<Array<Record<string, any>>>; useAPIQuery?: (index: number) => Promise<Array<Record<string, any>>>;
values?: Array<Record<string, any>>; values?: Array<Record<string, any>>;
@ -177,7 +177,6 @@ export interface GridlerState {
value: (current: GridlerStoreState[K]) => Partial<GridlerStoreState[K]> value: (current: GridlerStoreState[K]) => Partial<GridlerStoreState[K]>
) => Promise<void>; ) => Promise<void>;
toCell: <TRowType extends Record<string, string>>(row: any, col: number) => GridCell; toCell: <TRowType extends Record<string, string>>(row: any, col: number) => GridCell;
total_rows: number;
} }
export type GridlerStoreState = GridlerProps & GridlerState; export type GridlerStoreState = GridlerProps & GridlerState;
@ -850,10 +849,10 @@ const { Provider, useStore: useGridlerStore } = createSyncStore<GridlerStoreStat
return { return {
...props, ...props,
hasLocalData: props.data && props.data.length > 0,
hideMenu: props.hideMenu ?? menus.hide, hideMenu: props.hideMenu ?? menus.hide,
showMenu: props.showMenu ?? menus.show, showMenu: props.showMenu ?? menus.show,
total_rows: props.data?.length ?? getState('total_rows'), total_rows: props.total_rows ?? getState('total_rows') ?? 0,
}; };
} }
); );

View File

@ -1,8 +1,7 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { range } from '../utils/range'; import { range } from '../utils/range';
import { useGridlerStore } from './Store'; import { useGridlerStore } from './GridlerStore';
//The computer component does not need to be recalculated on every render, so we use React.memo to prevent unnecessary re-renders. //The computer component does not need to be recalculated on every render, so we use React.memo to prevent unnecessary re-renders.
export const Pager = React.memo(() => { export const Pager = React.memo(() => {
@ -14,7 +13,7 @@ export const Pager = React.memo(() => {
pageSize, pageSize,
loadPage, loadPage,
_loadingList, _loadingList,
hasLocalData hasLocalData,
] = useGridlerStore((s) => [ ] = useGridlerStore((s) => [
s.setState, s.setState,
s._glideref, s._glideref,
@ -24,17 +23,21 @@ export const Pager = React.memo(() => {
s.loadPage, s.loadPage,
s._loadingList, s._loadingList,
s.hasLocalData s.hasLocalData,
]); ]);
useEffect(() => { useEffect(() => {
if (!glideref) {return;} if (!glideref) {
return;
}
setState('mounted', true); setState('mounted', true);
}, [setState]); }, [setState]);
//Maybe move this into a computer component. //Maybe move this into a computer component.
useEffect(() => { useEffect(() => {
if (!glideref) {return;} if (!glideref) {
return;
}
if (hasLocalData) { if (hasLocalData) {
//using local data, no need to load pages //using local data, no need to load pages
return; return;
@ -56,7 +59,7 @@ export const Pager = React.memo(() => {
for (const page of range(firstPage, lastPage + 1, 1)) { for (const page of range(firstPage, lastPage + 1, 1)) {
loadPage(page); loadPage(page);
} }
}, [loadPage, pageSize, visiblePages, glideref, _loadingList,hasLocalData]); }, [loadPage, pageSize, visiblePages, glideref, _loadingList, hasLocalData]);
return <></>; return <></>;
}); });

View File

@ -1,7 +1,7 @@
import { ActionIcon } from '@mantine/core'; import { ActionIcon } from '@mantine/core';
import { IconMenu2 } from '@tabler/icons-react'; import { IconMenu2 } from '@tabler/icons-react';
import { useGridlerStore } from './Store'; import { useGridlerStore } from './GridlerStore';
export function RightMenuIcon() { export function RightMenuIcon() {
const { loadingData, onContextClick } = useGridlerStore((s) => ({ const { loadingData, onContextClick } = useGridlerStore((s) => ({

View File

@ -1,12 +1,12 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import type { APIOptions } from '../utils/types'; import type { APIOptions } from '../../utils/types';
import { useGridlerStore } from './Store'; import { useGridlerStore } from '../GridlerStore';
//The computer component does not need to be recalculated on every render, so we use React.memo to prevent unnecessary re-renders. //The computer component does not need to be recalculated on every render, so we use React.memo to prevent unnecessary re-renders.
export const APIAdaptorGoLangv2 = React.memo((props: APIOptions) => { export const GlidlerAPIAdaptorForGoLangv2 = React.memo((props: APIOptions) => {
const [setStateFN, setState, getState, addError, mounted] = useGridlerStore((s) => [ const [setStateFN, setState, getState, addError, mounted] = useGridlerStore((s) => [
s.setStateFN, s.setStateFN,
s.setState, s.setState,

View File

@ -7,13 +7,13 @@ import {
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect } from 'react';
import type { MantineBetterMenuInstanceItem } from '../../MantineBetterMenu'; import type { MantineBetterMenuInstanceItem } from '../../../MantineBetterMenu';
import type { FormRequestType } from '../utils/types'; import type { FormRequestType } from '../../utils/types';
import type { GridlerColumn } from './Column'; import type { GridlerColumn } from '../Column';
import { type GridlerProps, type GridlerState, useGridlerStore } from './Store'; import { type GridlerProps, type GridlerState, useGridlerStore } from '../GridlerStore';
export function GlidlerFormInterface(props: { export function GlidlerFormAdaptor(props: {
descriptionField?: ((data: Record<string, unknown>) => string) | string; descriptionField?: ((data: Record<string, unknown>) => string) | string;
getMenuItems?: GridlerProps['getMenuItems']; getMenuItems?: GridlerProps['getMenuItems'];
onReload?: () => void; onReload?: () => void;

View File

@ -1,23 +1,14 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import type { APIOptions } from '../utils/types'; import { useGridlerStore } from '../GridlerStore';
import { useGridlerStore } from './Store'; export interface GlidlerLocalDataAdaptorProps {
interface LocalDataAdaptorProps {
data: Array<unknown>; data: Array<unknown>;
} }
//The computer component does not need to be recalculated on every render, so we use React.memo to prevent unnecessary re-renders. //The computer component does not need to be recalculated on every render, so we use React.memo to prevent unnecessary re-renders.
export const LocalDataAdaptor = React.memo((props: LocalDataAdaptorProps) => { export const GlidlerLocalDataAdaptor = React.memo((props: GlidlerLocalDataAdaptorProps) => {
const [setStateFN, setState, getState, addError, mounted] = useGridlerStore((s) => [ const [setState, getState, mounted] = useGridlerStore((s) => [s.setState, s.getState, s.mounted]);
s.setStateFN,
s.setState,
s.getState,
s.addError,
s.mounted,
]);
const useAPIQuery: (index: number) => Promise<any> = async (index: number) => { const useAPIQuery: (index: number) => Promise<any> = async (index: number) => {
const pageSize = getState('pageSize'); const pageSize = getState('pageSize');

View File

@ -0,0 +1,3 @@
export * from './GlidlerAPIAdaptorForGoLangv2'
export * from './GlidlerFormAdaptor'
export * from './GlidlerLocalDataAdaptor'

View File

@ -3,7 +3,7 @@ import type { GetRowThemeCallback } from '@glideapps/glide-data-grid';
import { darken, lighten, useMantineColorScheme, useMantineTheme } from '@mantine/core'; import { darken, lighten, useMantineColorScheme, useMantineTheme } from '@mantine/core';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useGridlerStore } from '../components/Store'; import { useGridlerStore } from '../components/GridlerStore';
export const offsetRows = (colorScheme: any) => (i: number) => export const offsetRows = (colorScheme: any) => (i: number) =>
i % 2 === 0 i % 2 === 0

View File

@ -1,3 +1,6 @@
export {GlidlerAPIAdaptorForGoLangv2 } from './components/adaptors/GlidlerAPIAdaptorForGoLangv2'
export {GlidlerFormAdaptor } from './components/adaptors/GlidlerFormAdaptor'
export {GlidlerLocalDataAdaptor } from './components/adaptors/GlidlerLocalDataAdaptor'
export * from './components/Column' export * from './components/Column'
export {useGridlerStore } from './components/Store' export {useGridlerStore } from './components/GridlerStore'
export {Gridler} from './Gridler' export {Gridler} from './Gridler'

View File

@ -4,7 +4,7 @@ import { useState } from 'react';
import type { GridlerColumns } from '../components/Column'; import type { GridlerColumns } from '../components/Column';
import { APIAdaptorGoLangv2 } from '../components/APIAdaptorGoLangv2'; import { GlidlerAPIAdaptorForGoLangv2 } from '../components/adaptors';
import { Gridler } from '../Gridler'; import { Gridler } from '../Gridler';
export const GridlerGoAPIExampleEventlog = () => { export const GridlerGoAPIExampleEventlog = () => {
@ -51,9 +51,10 @@ export const GridlerGoAPIExampleEventlog = () => {
<TextInput label="API Key" onChange={(e) => setApiKey(e.target.value)} value={apiKey} /> <TextInput label="API Key" onChange={(e) => setApiKey(e.target.value)} value={apiKey} />
<Divider /> <Divider />
<Checkbox <Checkbox
label="Show Side Sections"
checked={!!sections} checked={!!sections}
label="Show Side Sections"
onChange={(e) => { onChange={(e) => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
e.target.checked e.target.checked
? setSections({ ? setSections({
bottom: <div style={{ backgroundColor: 'teal', height: '25px' }}>bottom</div>, bottom: <div style={{ backgroundColor: 'teal', height: '25px' }}>bottom</div>,
@ -66,8 +67,8 @@ export const GridlerGoAPIExampleEventlog = () => {
/> />
<Divider /> <Divider />
<Gridler <Gridler
height="100%"
columns={columns} columns={columns}
height="100%"
// getMenuItems={(id, _state, row, col, defaultItems) => { // getMenuItems={(id, _state, row, col, defaultItems) => {
// console.log('GridlerGoAPIExampleEventlog getMenuItems root', id, row, col, defaultItems); // console.log('GridlerGoAPIExampleEventlog getMenuItems root', id, row, col, defaultItems);
// return [ // return [
@ -92,8 +93,8 @@ export const GridlerGoAPIExampleEventlog = () => {
uniqueid="gridtest" uniqueid="gridtest"
values={values} values={values}
> >
<Gridler.APIAdaptorGoLangv2 authtoken={apiKey} url={`${apiUrl}/public/process`} /> <GlidlerAPIAdaptorForGoLangv2 authtoken={apiKey} url={`${apiUrl}/public/process`} />
<Gridler.GlidlerFormInterface <Gridler.FormAdaptor
descriptionField={'process'} descriptionField={'process'}
onRequestForm={(request, data) => { onRequestForm={(request, data) => {
console.log('Form requested', request, data); console.log('Form requested', request, data);

View File

@ -1,19 +1,17 @@
import { MantineProvider } from '@mantine/core' import { MantineProvider } from '@mantine/core';
import { render, screen } from '@testing-library/react' import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest' import { describe, expect, it, vi } from 'vitest';
import { MantineBetterMenusProvider } from './MantineBetterMenu' import { MantineBetterMenusProvider } from './MantineBetterMenu';
// Mock the MenuRenderer component since it likely has complex portal logic // Mock the MenuRenderer component since it likely has complex portal logic
vi.mock('./MenuRenderer', () => ({ vi.mock('./MenuRenderer', () => ({
MenuRenderer: () => <div data-testid="menu-renderer">Menu Renderer</div> MenuRenderer: () => <div data-testid="menu-renderer">Menu Renderer</div>,
})) }));
const TestWrapper = ({ children }: { children: React.ReactNode }) => ( const TestWrapper = ({ children }: { children: React.ReactNode }) => (
<MantineProvider> <MantineProvider>{children}</MantineProvider>
{children} );
</MantineProvider>
)
describe('MantineBetterMenusProvider', () => { describe('MantineBetterMenusProvider', () => {
it('renders children correctly', () => { it('renders children correctly', () => {
@ -23,10 +21,10 @@ describe('MantineBetterMenusProvider', () => {
<div data-testid="test-child">Test Child</div> <div data-testid="test-child">Test Child</div>
</MantineBetterMenusProvider> </MantineBetterMenusProvider>
</TestWrapper> </TestWrapper>
) );
expect(screen.getByTestId('test-child')).toBeInTheDocument() expect(screen.getByTestId('test-child')).toBeInTheDocument();
}) });
it('renders MenuRenderer component', () => { it('renders MenuRenderer component', () => {
render( render(
@ -35,10 +33,10 @@ describe('MantineBetterMenusProvider', () => {
<div>Test</div> <div>Test</div>
</MantineBetterMenusProvider> </MantineBetterMenusProvider>
</TestWrapper> </TestWrapper>
) );
expect(screen.getByTestId('menu-renderer')).toBeInTheDocument() expect(screen.getByTestId('menu-renderer')).toBeInTheDocument();
}) });
it('accepts providerID prop', () => { it('accepts providerID prop', () => {
render( render(
@ -47,9 +45,9 @@ describe('MantineBetterMenusProvider', () => {
<div>Test</div> <div>Test</div>
</MantineBetterMenusProvider> </MantineBetterMenusProvider>
</TestWrapper> </TestWrapper>
) );
// Component should render without errors // Component should render without errors
expect(screen.getByTestId('menu-renderer')).toBeInTheDocument() expect(screen.getByTestId('menu-renderer')).toBeInTheDocument();
}) });
}) });

View File

@ -1,7 +1,9 @@
import { MenuRenderer } from './MenuRenderer'; import { MenuRenderer } from './MenuRenderer';
import { MantineBetterMenusStoreProvider, type MenuStoreProps } from './Store'; import { MantineBetterMenusStoreProvider, type MantineBetterMenuStoreProps } from './Store';
export function MantineBetterMenusProvider(props: React.PropsWithChildren<MenuStoreProps>) { export function MantineBetterMenusProvider(
props: React.PropsWithChildren<MantineBetterMenuStoreProps>
) {
return ( return (
<MantineBetterMenusStoreProvider {...props}> <MantineBetterMenusStoreProvider {...props}>
<MenuRenderer /> <MenuRenderer />

View File

@ -1,100 +1,98 @@
import { act, renderHook } from '@testing-library/react' import { act, renderHook } from '@testing-library/react';
import { beforeEach, describe, expect, it } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest';
import { MantineBetterMenusStoreProvider, useMantineBetterMenus } from './Store' import { MantineBetterMenusStoreProvider, useMantineBetterMenus } from './Store';
// Mock dependencies // Mock dependencies
vi.mock('@warkypublic/artemis-kit', () => ({ vi.mock('@warkypublic/artemis-kit', () => ({
getUUID: () => 'test-uuid-123' getUUID: () => 'test-uuid-123',
})) }));
const createWrapper = () => { const createWrapper = () => {
return ({ children }: { children: React.ReactNode }) => ( return ({ children }: { children: React.ReactNode }) => (
<MantineBetterMenusStoreProvider> <MantineBetterMenusStoreProvider>{children}</MantineBetterMenusStoreProvider>
{children} );
</MantineBetterMenusStoreProvider> };
)
}
describe('MantineBetterMenus Store', () => { describe('MantineBetterMenus Store', () => {
let wrapper: ReturnType<typeof createWrapper> let wrapper: ReturnType<typeof createWrapper>;
beforeEach(() => { beforeEach(() => {
wrapper = createWrapper() wrapper = createWrapper();
}) });
it('initializes with empty menus array', () => { it('initializes with empty menus array', () => {
const { result } = renderHook(() => useMantineBetterMenus(), { wrapper }) const { result } = renderHook(() => useMantineBetterMenus(), { wrapper });
expect(result.current.menus).toEqual([]) expect(result.current.menus).toEqual([]);
}) });
it('can show a menu', () => { it('can show a menu', () => {
const { result } = renderHook(() => useMantineBetterMenus(), { wrapper }) const { result } = renderHook(() => useMantineBetterMenus(), { wrapper });
act(() => { act(() => {
result.current.show('test-menu', { result.current.show('test-menu', {
items: [{ label: 'Test Item' }], items: [{ label: 'Test Item' }],
x: 100, x: 100,
y: 200 y: 200,
}) });
}) });
expect(result.current.menus).toHaveLength(1) expect(result.current.menus).toHaveLength(1);
expect(result.current.menus[0]).toMatchObject({ expect(result.current.menus[0]).toMatchObject({
id: 'test-menu', id: 'test-menu',
items: [{ label: 'Test Item' }], items: [{ label: 'Test Item' }],
visible: true, visible: true,
x: 100, x: 100,
y: 200 y: 200,
}) });
}) });
it('can hide a menu', () => { it('can hide a menu', () => {
const { result } = renderHook(() => useMantineBetterMenus(), { wrapper }) const { result } = renderHook(() => useMantineBetterMenus(), { wrapper });
// First show a menu // First show a menu
act(() => { act(() => {
result.current.show('test-menu', { x: 100, y: 200 }) result.current.show('test-menu', { x: 100, y: 200 });
}) });
expect(result.current.menus[0].visible).toBe(true) expect(result.current.menus[0].visible).toBe(true);
// Then hide it // Then hide it
act(() => { act(() => {
result.current.hide('test-menu') result.current.hide('test-menu');
}) });
expect(result.current.menus[0].visible).toBe(false) expect(result.current.menus[0].visible).toBe(false);
}) });
it('can update instance state', () => { it('can update instance state', () => {
const { result } = renderHook(() => useMantineBetterMenus(), { wrapper }) const { result } = renderHook(() => useMantineBetterMenus(), { wrapper });
// Show a menu // Show a menu
act(() => { act(() => {
result.current.show('test-menu', { x: 100, y: 200 }) result.current.show('test-menu', { x: 100, y: 200 });
}) });
// Update its position // Update its position
act(() => { act(() => {
result.current.setInstanceState('test-menu', 'x', 300) result.current.setInstanceState('test-menu', 'x', 300);
}) });
expect(result.current.menus[0].x).toBe(300) expect(result.current.menus[0].x).toBe(300);
expect(result.current.menus[0].y).toBe(200) // Should remain unchanged expect(result.current.menus[0].y).toBe(200); // Should remain unchanged
}) });
it('handles multiple menus', () => { it('handles multiple menus', () => {
const { result } = renderHook(() => useMantineBetterMenus(), { wrapper }) const { result } = renderHook(() => useMantineBetterMenus(), { wrapper });
act(() => { act(() => {
result.current.show('menu-1', { x: 100, y: 200 }) result.current.show('menu-1', { x: 100, y: 200 });
result.current.show('menu-2', { x: 300, y: 400 }) result.current.show('menu-2', { x: 300, y: 400 });
}) });
expect(result.current.menus).toHaveLength(2) expect(result.current.menus).toHaveLength(2);
expect(result.current.menus.find(m => m.id === 'menu-1')).toBeDefined() expect(result.current.menus.find((m) => m.id === 'menu-1')).toBeDefined();
expect(result.current.menus.find(m => m.id === 'menu-2')).toBeDefined() expect(result.current.menus.find((m) => m.id === 'menu-2')).toBeDefined();
}) });
}) });

View File

@ -28,15 +28,16 @@ export interface MantineBetterMenuInstanceItem extends Partial<MenuItemProps> {
| ReactNode; | ReactNode;
} }
export interface MenuStoreProps { export interface MantineBetterMenuStoreProps {
menus?: Array<MantineBetterMenuInstance>; menus?: Array<MantineBetterMenuInstance>;
providerID?: string; providerID?: string;
width?: number; width?: number;
} }
export type MenuStoreState = MenuStoreProps & MenuStoreStateOnly; export type MantineBetterMenuStoreState = MantineBetterMenuStoreProps &
MantineBetterMenuStoreStateOnly;
export interface MenuStoreStateOnly { export interface MantineBetterMenuStoreStateOnly {
hide: (id: string) => void; hide: (id: string) => void;
menus: Array<MantineBetterMenuInstance>; menus: Array<MantineBetterMenuInstance>;
setInstanceState: <K extends keyof MantineBetterMenuInstance>( setInstanceState: <K extends keyof MantineBetterMenuInstance>(
@ -44,12 +45,15 @@ export interface MenuStoreStateOnly {
key: K, key: K,
value: MantineBetterMenuInstance[K] value: MantineBetterMenuInstance[K]
) => void; ) => void;
setState: <K extends keyof MenuStoreState>(key: K, value: Partial<MenuStoreState[K]>) => void; setState: <K extends keyof MantineBetterMenuStoreState>(
key: K,
value: Partial<MantineBetterMenuStoreState[K]>
) => void;
show: (id: string, options?: Partial<MantineBetterMenuInstance>) => void; show: (id: string, options?: Partial<MantineBetterMenuInstance>) => void;
} }
const { Provider: MantineBetterMenusStoreProvider, useStore: useMantineBetterMenus } = const { Provider: MantineBetterMenusStoreProvider, useStore: useMantineBetterMenus } =
createSyncStore<MenuStoreState, MenuStoreProps>( createSyncStore<MantineBetterMenuStoreState, MantineBetterMenuStoreProps>(
(set, get) => ({ (set, get) => ({
hide: (id: string) => { hide: (id: string) => {
const s = get(); const s = get();
@ -59,7 +63,7 @@ const { Provider: MantineBetterMenusStoreProvider, useStore: useMantineBetterMen
setInstanceState: (id, key, value) => { setInstanceState: (id, key, value) => {
//@ts-expect-error Type instantiation is excessively deep and possibly infinite. //@ts-expect-error Type instantiation is excessively deep and possibly infinite.
set( set(
produce((state: MenuStoreState) => { produce((state: MantineBetterMenuStoreState) => {
const idx = state?.menus?.findIndex((m: MantineBetterMenuInstance) => m.id === id); const idx = state?.menus?.findIndex((m: MantineBetterMenuInstance) => m.id === id);
if (idx >= 0) { if (idx >= 0) {
state.menus[idx][key] = value; state.menus[idx][key] = value;
@ -89,7 +93,7 @@ const { Provider: MantineBetterMenusStoreProvider, useStore: useMantineBetterMen
s.setState('menus', [...s.menus, menu as MantineBetterMenuInstance]); s.setState('menus', [...s.menus, menu as MantineBetterMenuInstance]);
} else { } else {
set( set(
produce((state: MenuStoreState) => { produce((state: MantineBetterMenuStoreState) => {
if (!state.menus) { if (!state.menus) {
state.menus = []; state.menus = [];
} }

View File

@ -3,5 +3,5 @@ export { useMantineBetterMenus } from './Store';
export type { export type {
MantineBetterMenuInstance, MantineBetterMenuInstance,
MantineBetterMenuInstanceItem, MantineBetterMenuInstanceItem,
MenuStoreState MantineBetterMenuStoreState
} from './Store'; } from './Store';

View File

@ -5,6 +5,6 @@ export {
type MantineBetterMenuInstance, type MantineBetterMenuInstance,
type MantineBetterMenuInstanceItem, type MantineBetterMenuInstanceItem,
MantineBetterMenusProvider, MantineBetterMenusProvider,
type MenuStoreState, type MantineBetterMenuStoreState,
useMantineBetterMenus, useMantineBetterMenus,
} from "./MantineBetterMenu"; } from "./MantineBetterMenu";