Grid selection

This commit is contained in:
Hein 2025-10-20 16:00:21 +02:00
parent 6350b513ca
commit 4186219c50
5 changed files with 246 additions and 81 deletions

View File

@ -1,3 +1,10 @@
import {
IconEdit,
IconExclamationMark,
IconRefresh,
IconSquarePlus,
IconTrashX,
} from '@tabler/icons-react';
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect } from 'react';
import type { MantineBetterMenuInstanceItem } from '../../MantineBetterMenu'; import type { MantineBetterMenuInstanceItem } from '../../MantineBetterMenu';
@ -7,9 +14,14 @@ import type { GridlerColumn } from './Column';
import { type GridlerProps, type GridlerState, useGridlerStore } from './Store'; import { type GridlerProps, type GridlerState, useGridlerStore } from './Store';
export function GlidlerFormInterface(props: { export function GlidlerFormInterface(props: {
descriptionField?: ((data: Record<string, unknown>) => string) | string;
getMenuItems?: GridlerProps['getMenuItems']; getMenuItems?: GridlerProps['getMenuItems'];
onReload?: () => void; onReload?: () => void;
onRequestForm: (request: FormRequestType, data: Record<string, unknown>) => void; onRequestForm: (
request: FormRequestType,
data: Array<Record<string, unknown>> | Record<string, unknown>
) => void;
showDescriptionInMenu?: boolean;
}) { }) {
const [getState, mounted, setState, reload] = useGridlerStore((s) => [ const [getState, mounted, setState, reload] = useGridlerStore((s) => [
s.getState, s.getState,
@ -22,7 +34,7 @@ export function GlidlerFormInterface(props: {
( (
id: string, id: string,
storeState: GridlerState, storeState: GridlerState,
row?: unknown, row?: Record<string, unknown>,
col?: GridlerColumn, col?: GridlerColumn,
defaultItems?: Array<unknown> defaultItems?: Array<unknown>
) => { ) => {
@ -33,13 +45,26 @@ export function GlidlerFormInterface(props: {
} }
const items = [] as Array<MantineBetterMenuInstanceItem>; const items = [] as Array<MantineBetterMenuInstanceItem>;
if (defaultItems && id === 'cell') {
items.push(...(defaultItems as Array<MantineBetterMenuInstanceItem>));
}
const rows = getState('_gridSelection')?.rows.toArray() ?? [];
const manyRows = rows.length > 1;
if (!row) { if (!row) {
const firstRow = getState('_gridSelection')?.rows?.first(); const firstRow = rows[0];
if (firstRow !== undefined) { if (firstRow !== undefined) {
row = storeState.getRowBuffer(firstRow); row = storeState.getRowBuffer(firstRow);
} }
} }
const desc =
typeof props.descriptionField === 'string'
? row?.[props.descriptionField]
: typeof props.descriptionField === 'function' && row
? props.descriptionField(row)
: undefined;
if (id === 'other') { if (id === 'other') {
items.push({ items.push({
c: 'blue', c: 'blue',
@ -51,35 +76,70 @@ export function GlidlerFormInterface(props: {
} }
if ((id === 'cell' && row) || (id === 'menu' && row)) { if ((id === 'cell' && row) || (id === 'menu' && row)) {
items.push({ items.push({
c: 'blue', c: 'teal',
label: 'Add', label: 'Add',
leftSection: <IconSquarePlus color="teal" size={16} />,
onClick: () => { onClick: () => {
props.onRequestForm('insert', row as Record<string, unknown>); props.onRequestForm('insert', row as Record<string, unknown>);
}, },
}); });
if (!manyRows) {
items.push({
c: 'green',
label: `Modify${desc && props.showDescriptionInMenu ? ` (${desc})` : ''}`,
leftSection: <IconEdit color="green" size={16} />,
onClick: () => {
props.onRequestForm('change', row as Record<string, unknown>);
},
});
items.push({
c: 'red',
label: `Remove${desc && props.showDescriptionInMenu ? ` (${desc})` : ''}`,
leftSection: <IconTrashX color="maroon" size={16} />,
onClick: () => {
props.onRequestForm('delete', row as Record<string, unknown>);
},
});
} else {
items.push({
c: 'green',
label: `Modify All Selected (${rows.length})`,
leftSection: <IconEdit color="green" size={16} />,
onClick: () => {
props.onRequestForm(
'change',
rows.map((r) => storeState.getRowBuffer(r)) as Array<Record<string, unknown>>
);
},
});
items.push({
c: 'red',
label: `Remove All Selected (${rows.length})`,
leftSection: <IconTrashX color="maroon" size={16} />,
onClick: () => {
props.onRequestForm(
'delete',
rows.map((r) => storeState.getRowBuffer(r)) as Array<Record<string, unknown>>
);
},
});
}
items.push({ items.push({
c: 'green', isDivider: true,
label: 'Change',
onClick: () => {
props.onRequestForm('change', row as Record<string, unknown>);
},
}); });
} else if ((id === 'cell' && !row) || (id === 'menu' && !row)) {
items.push({ items.push({
c: 'red', c: 'red',
label: 'Delete', label: `Nothing Selected`,
onClick: () => { leftSection: <IconExclamationMark color="yellow" size={16} />,
props.onRequestForm('delete', row as Record<string, unknown>);
},
}); });
} }
items.push({
isDivider: true,
});
items.push({ items.push({
c: 'orange', c: 'orange',
label: 'Refresh', label: 'Refresh',
leftSection: <IconRefresh color="orange" size={16} />,
onClick: () => { onClick: () => {
reload?.(); reload?.();
}, },

View File

@ -32,6 +32,7 @@ import { ColumnFilterSet, type GridlerColumn, type GridlerColumns } from './Colu
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';
@ -418,14 +419,65 @@ const { Provider, useStore: useGridlerStore } = createSyncStore<GridlerStoreStat
: coldef : coldef
? [ ? [
{ {
label: `Column ${coldef?.title ?? coldef?.id} Row ${row}`, 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} />,
}, },
] ]
: [ : [];
{
label: `No Column ${area}`,
},
];
s.hideMenu?.(area); s.hideMenu?.(area);
s.showMenu?.(area, { s.showMenu?.(area, {

View File

@ -94,6 +94,7 @@ export const GridlerGoAPIExampleEventlog = () => {
> >
<Gridler.APIAdaptorGoLangv2 authtoken={apiKey} url={`${apiUrl}/public/process`} /> <Gridler.APIAdaptorGoLangv2 authtoken={apiKey} url={`${apiUrl}/public/process`} />
<Gridler.GlidlerFormInterface <Gridler.GlidlerFormInterface
descriptionField={'process'}
onRequestForm={(request, data) => { onRequestForm={(request, data) => {
console.log('Form requested', request, data); console.log('Form requested', request, data);
}} }}

View File

@ -1,14 +1,15 @@
import { Menu, Portal } from '@mantine/core'; import { Menu, Portal } from '@mantine/core';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { type MantineBetterMenuInstanceItem, useMantineBetterMenus } from './Store'; import { type MantineBetterMenuInstanceItem, useMantineBetterMenus } from './Store';
export function MenuRenderer() { export function MenuRenderer() {
const { menus, providerID, setInstanceState } = useMantineBetterMenus((s) => ({ const { menus, providerID, setInstanceState, width } = useMantineBetterMenus((s) => ({
menus: s.menus, menus: s.menus,
providerID: s.providerID, providerID: s.providerID,
setInstanceState: s.setInstanceState, setInstanceState: s.setInstanceState,
setState: s.setState setState: s.setState,
width: s.width,
})); }));
return ( return (
@ -18,7 +19,7 @@ export function MenuRenderer() {
return ( return (
<Menu <Menu
shadow="md" shadow="md"
width={200} width={width ?? '300'}
{...m.menuProps} {...m.menuProps}
key={`bmm_menu_${providerID}_${menuIndex}`} key={`bmm_menu_${providerID}_${menuIndex}`}
onClose={() => { onClose={() => {
@ -68,8 +69,47 @@ const MenuItemRenderer = ({ children, label, ...props }: MantineBetterMenuInstan
return <Menu.Divider />; return <Menu.Divider />;
} }
if (props.items && props.items.length > 0) {
return (
<Menu.Sub>
<Menu.Sub.Target>
<Menu.Sub.Item
{...props}
disabled={loading}
onClick={(e) => {
props.onClick?.(e);
if (props.onClickAsync) {
setLoading(true);
props.onClickAsync().finally(() => setLoading(false));
}
}}
styles={{
itemLabel: {
overflow: 'auto',
wordWrap: 'break-word',
},
...props.styles,
}}
>
{children ?? label}
</Menu.Sub.Item>
</Menu.Sub.Target>
<Menu.Sub.Dropdown>
{React.Children.toArray(
props.items.map((subitem, subitemIndex) => (
<MenuItemRenderer
key={`bmm_subitem_${subitem?.id ?? ''}${subitemIndex}`}
{...subitem}
/>
))
)}
</Menu.Sub.Dropdown>
</Menu.Sub>
);
}
if (!props.onClick && !props.onClickAsync) { if (!props.onClick && !props.onClickAsync) {
return <Menu.Label {...(props as Record<string,unknown>)}> {children ?? label}</Menu.Label>; return <Menu.Label {...(props as Record<string, unknown>)}> {children ?? label}</Menu.Label>;
} }
return ( return (
@ -77,12 +117,19 @@ const MenuItemRenderer = ({ children, label, ...props }: MantineBetterMenuInstan
{...props} {...props}
disabled={loading} disabled={loading}
onClick={(e) => { onClick={(e) => {
props.onClick?.(e ); props.onClick?.(e);
if (props.onClickAsync) { if (props.onClickAsync) {
setLoading(true); setLoading(true);
props.onClickAsync().finally(() => setLoading(false)); props.onClickAsync().finally(() => setLoading(false));
} }
}} }}
styles={{
itemLabel: {
overflow: 'auto',
wordWrap: 'break-word',
},
...props.styles,
}}
> >
{children ?? label} {children ?? label}
</Menu.Item> </Menu.Item>

View File

@ -11,12 +11,15 @@ export interface MantineBetterMenuInstance {
menuProps?: MenuProps; menuProps?: MenuProps;
renderer?: ReactNode; renderer?: ReactNode;
visible: boolean; visible: boolean;
x: number; x: number;
y: number; y: number;
} }
export interface MantineBetterMenuInstanceItem extends Partial<MenuItemProps> { export interface MantineBetterMenuInstanceItem extends Partial<MenuItemProps> {
id?: string;
isDivider?: boolean; isDivider?: boolean;
items?: Array<MantineBetterMenuInstanceItem>;
label?: string; label?: string;
onClick?: (e?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void; onClick?: (e?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
onClickAsync?: () => Promise<void>; onClickAsync?: () => Promise<void>;
@ -28,6 +31,7 @@ export interface MantineBetterMenuInstanceItem extends Partial<MenuItemProps> {
export interface MenuStoreProps { export interface MenuStoreProps {
menus?: Array<MantineBetterMenuInstance>; menus?: Array<MantineBetterMenuInstance>;
providerID?: string; providerID?: string;
width?: number;
} }
export type MenuStoreState = MenuStoreProps & MenuStoreStateOnly; export type MenuStoreState = MenuStoreProps & MenuStoreStateOnly;
@ -44,61 +48,62 @@ export interface MenuStoreStateOnly {
show: (id: string, options?: Partial<MantineBetterMenuInstance>) => void; show: (id: string, options?: Partial<MantineBetterMenuInstance>) => void;
} }
const { Provider:MantineBetterMenusStoreProvider, useStore:useMantineBetterMenus } = createSyncStore<MenuStoreState, MenuStoreProps>( const { Provider: MantineBetterMenusStoreProvider, useStore: useMantineBetterMenus } =
(set, get) => ({ createSyncStore<MenuStoreState, MenuStoreProps>(
hide: (id: string) => { (set, get) => ({
const s = get(); hide: (id: string) => {
s.setInstanceState(id, 'visible', false); const s = get();
}, s.setInstanceState(id, 'visible', false);
menus: [], },
setInstanceState: (id, key, value) => { menus: [],
//@ts-expect-error Type instantiation is excessively deep and possibly infinite. setInstanceState: (id, key, value) => {
set( //@ts-expect-error Type instantiation is excessively deep and possibly infinite.
produce((state: MenuStoreState) => {
const idx = state?.menus?.findIndex((m: MantineBetterMenuInstance) => m.id === id);
if (idx >= 0) {
state.menus[idx][key] = value;
}
})
);
},
setState: (key, value) => {
set(
produce((state) => {
state[key] = value;
})
);
},
show: (id: string, options?: Partial<Omit<MantineBetterMenuInstance, 'id'>>) => {
const s = get();
const menuIndex = s.menus.findIndex((m) => m.id === id);
const menu: Partial<MantineBetterMenuInstance> = s.menus[menuIndex]
? { ...s.menus[menuIndex] }
: {};
Object.assign(menu, options);
menu.id = menu.id ?? id;
menu.visible = !(menu.visible ?? false);
if (menuIndex < 0) {
s.setState('menus', [...s.menus, menu as MantineBetterMenuInstance]);
} else {
set( set(
produce((state: MenuStoreState) => { produce((state: MenuStoreState) => {
if (!state.menus) { const idx = state?.menus?.findIndex((m: MantineBetterMenuInstance) => m.id === id);
state.menus = []; if (idx >= 0) {
state.menus[idx][key] = value;
} }
state.menus[menuIndex] = { ...state.menus[menuIndex], ...menu };
}) })
); );
} },
} setState: (key, value) => {
}), set(
(props) => { produce((state) => {
return { state[key] = value;
providerID: props.providerID ?? `MenuStore-${getUUID()}` })
}; );
} },
); show: (id: string, options?: Partial<Omit<MantineBetterMenuInstance, 'id'>>) => {
const s = get();
const menuIndex = s.menus.findIndex((m) => m.id === id);
const menu: Partial<MantineBetterMenuInstance> = s.menus[menuIndex]
? { ...s.menus[menuIndex] }
: {};
export { MantineBetterMenusStoreProvider,useMantineBetterMenus }; Object.assign(menu, options);
menu.id = menu.id ?? id;
menu.visible = !(menu.visible ?? false);
if (menuIndex < 0) {
s.setState('menus', [...s.menus, menu as MantineBetterMenuInstance]);
} else {
set(
produce((state: MenuStoreState) => {
if (!state.menus) {
state.menus = [];
}
state.menus[menuIndex] = { ...state.menus[menuIndex], ...menu };
})
);
}
},
}),
(props) => {
return {
providerID: props.providerID ?? `MenuStore-${getUUID()}`,
};
}
);
export { MantineBetterMenusStoreProvider, useMantineBetterMenus };