@linzjs/step-ag-grid 32.0.0 → 32.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/components/types.d.ts +2 -0
- package/dist/step-ag-grid.cjs +12 -3
- package/dist/step-ag-grid.cjs.map +1 -1
- package/dist/step-ag-grid.esm.js +13 -4
- package/dist/step-ag-grid.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Grid.tsx +6 -3
- package/src/components/gridForm/GridFormPopoverMenu.tsx +6 -2
- package/src/components/gridPopoverEdit/GridPopoverMenu.tsx +3 -0
- package/src/components/types.ts +2 -0
- package/src/stories/grid/GridReadOnly.stories.tsx +69 -0
- package/src/stories/grid/gridFormInteraction/GridFormPopoverMenuInteraction.stories.tsx +47 -0
package/package.json
CHANGED
package/src/components/Grid.tsx
CHANGED
|
@@ -44,7 +44,7 @@ import { useGridCopy } from './gridHook/useGridCopy';
|
|
|
44
44
|
import { CellLocation, useGridRangeSelection } from './gridHook/useGridRangeSelection';
|
|
45
45
|
import { GridNoRowsOverlay } from './GridNoRowsOverlay';
|
|
46
46
|
import { usePostSortRowsHook } from './PostSortRowsHook';
|
|
47
|
-
import { GridBaseRow, GridOnRowDragEndProps } from './types';
|
|
47
|
+
import { ColDefT, GridBaseRow, GridOnRowDragEndProps } from './types';
|
|
48
48
|
|
|
49
49
|
ModuleRegistry.registerModules([AllCommunityModule]);
|
|
50
50
|
|
|
@@ -471,13 +471,16 @@ export const Grid = <TData extends GridBaseRow = GridBaseRow>({
|
|
|
471
471
|
};
|
|
472
472
|
} else {
|
|
473
473
|
const colDefEditable = colDef.editable;
|
|
474
|
+
const ignoreGridReadOnly = (colDef as ColDefT<TData>).ignoreGridReadOnly === true;
|
|
474
475
|
const editable = combineEditables(
|
|
475
|
-
params.loading !== true
|
|
476
|
+
params.loading !== true,
|
|
477
|
+
ignoreGridReadOnly ? true : params.readOnly !== true,
|
|
476
478
|
params.defaultColDef?.editable,
|
|
477
479
|
colDefEditable,
|
|
478
480
|
);
|
|
479
481
|
return {
|
|
480
|
-
|
|
482
|
+
// ag-grid warns on unrecognised colDef properties, so it can't be left on the returned colDef
|
|
483
|
+
...omit(colDef, 'ignoreGridReadOnly'),
|
|
481
484
|
editable,
|
|
482
485
|
cellClassRules: {
|
|
483
486
|
...colDef.cellClassRules,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { isEmpty } from 'lodash-es';
|
|
2
1
|
import { Fragment, ReactElement, useCallback, useEffect, useRef, useState } from 'react';
|
|
3
2
|
|
|
4
3
|
import { useGridPopoverContext } from '../../contexts/GridPopoverContext';
|
|
@@ -126,10 +125,15 @@ export const GridFormPopoverMenu = <TData extends GridBaseRow>(props: GridFormPo
|
|
|
126
125
|
save,
|
|
127
126
|
});
|
|
128
127
|
|
|
128
|
+
// isEmpty(options) alone isn't enough: options can be a non-empty array where every item is
|
|
129
|
+
// hidden (e.g. all actions unavailable in a given mode), which must also show "No actions"
|
|
130
|
+
// rather than rendering an empty menu body.
|
|
131
|
+
const hasVisibleOption = options?.some((item) => !item.hidden && item.label !== __isMenuSeparator__);
|
|
132
|
+
|
|
129
133
|
return popoverWrapper(
|
|
130
134
|
<ComponentLoadingWrapper loading={!options} className={'GridFormPopupMenu'}>
|
|
131
135
|
<>
|
|
132
|
-
{
|
|
136
|
+
{!hasVisibleOption ? (
|
|
133
137
|
<MenuItem key={`GridPopoverMenu-empty`} className={'GridPopoverMenu-noOptions'} disabled={true}>
|
|
134
138
|
No actions
|
|
135
139
|
</MenuItem>
|
|
@@ -20,6 +20,9 @@ export const GridPopoverMenu = <TData extends GridBaseRow>(
|
|
|
20
20
|
editable: colDef.editable != null ? colDef.editable : true,
|
|
21
21
|
exportable: false,
|
|
22
22
|
cellStyle: { flex: 1, justifyContent: 'center' },
|
|
23
|
+
suppressMovable: true,
|
|
24
|
+
lockPosition: 'right',
|
|
25
|
+
resizable: false,
|
|
23
26
|
cellRenderer: GridRenderPopoutMenuCell,
|
|
24
27
|
// Menus open on single click, this parameter is picked up in Grid.tsx
|
|
25
28
|
singleClickEdit: true,
|
package/src/components/types.ts
CHANGED
|
@@ -20,6 +20,8 @@ export interface GridOnRowDragEndProps<TData extends GridBaseRow> {
|
|
|
20
20
|
// This is so that typescript retains the row type to pass to the GridCells
|
|
21
21
|
export interface ColDefT<TData extends GridBaseRow, ValueType = any> extends ColDef<TData, ValueType> {
|
|
22
22
|
editable?: boolean | EditableCallback<TData, ValueType>;
|
|
23
|
+
/** When true, this column's editable state ignores the grid-level `readOnly` prop. */
|
|
24
|
+
ignoreGridReadOnly?: boolean;
|
|
23
25
|
valueGetter?: string | ValueGetterFunc<TData, ValueType>;
|
|
24
26
|
valueFormatter?: string | ValueFormatterFunc<TData, ValueType>;
|
|
25
27
|
cellRenderer?:
|
|
@@ -5,6 +5,7 @@ import '@linzjs/lui/dist/fonts';
|
|
|
5
5
|
|
|
6
6
|
import { Meta, StoryFn } from '@storybook/react-vite';
|
|
7
7
|
import { ReactElement, useCallback, useMemo, useState } from 'react';
|
|
8
|
+
import { expect, fn, screen, userEvent } from 'storybook/test';
|
|
8
9
|
|
|
9
10
|
import {
|
|
10
11
|
ColDefT,
|
|
@@ -294,3 +295,71 @@ const GridFilterLessThan = (props: {
|
|
|
294
295
|
|
|
295
296
|
export const ReadOnlySingleSelection = GridReadOnlyTemplate.bind({});
|
|
296
297
|
ReadOnlySingleSelection.play = waitForGridReady;
|
|
298
|
+
|
|
299
|
+
interface IIgnoreReadOnlyRow {
|
|
300
|
+
id: number;
|
|
301
|
+
name: string;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const disabledMenuItemAction = fn();
|
|
305
|
+
|
|
306
|
+
const IgnoreGridReadOnlyTemplate: StoryFn<typeof Grid<IIgnoreReadOnlyRow>> = () => {
|
|
307
|
+
const columnDefs: ColDefT<IIgnoreReadOnlyRow>[] = useMemo(
|
|
308
|
+
() => [
|
|
309
|
+
GridCell<IIgnoreReadOnlyRow, IIgnoreReadOnlyRow['name']>({ field: 'name', headerName: 'Name' }),
|
|
310
|
+
GridPopoverMenu(
|
|
311
|
+
{ colId: 'blockedActions', headerName: 'Blocked actions' },
|
|
312
|
+
{
|
|
313
|
+
multiEdit: true,
|
|
314
|
+
editorParams: {
|
|
315
|
+
options: async () => [{ label: 'Should not open', action: async () => {} }],
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
),
|
|
319
|
+
GridPopoverMenu(
|
|
320
|
+
{ colId: 'alwaysEnabledActions', headerName: 'Always enabled actions', ignoreGridReadOnly: true },
|
|
321
|
+
{
|
|
322
|
+
multiEdit: true,
|
|
323
|
+
editorParams: {
|
|
324
|
+
options: async () => [
|
|
325
|
+
{ label: 'Locate feature', action: async () => {} },
|
|
326
|
+
{ label: 'Other action', action: disabledMenuItemAction, disabled: 'Not available in read-only mode' },
|
|
327
|
+
],
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
),
|
|
331
|
+
],
|
|
332
|
+
[],
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
const [rowData] = useState<IIgnoreReadOnlyRow[]>([{ id: 1, name: 'Row 1' }]);
|
|
336
|
+
|
|
337
|
+
return (
|
|
338
|
+
<GridWrapper maxHeight={200}>
|
|
339
|
+
<Grid data-testid={'readonly-ignore-flag'} readOnly columnDefs={columnDefs} rowData={rowData} />
|
|
340
|
+
</GridWrapper>
|
|
341
|
+
);
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
export const ReadOnlyIgnoreGridReadOnlyColumn = IgnoreGridReadOnlyTemplate.bind({});
|
|
345
|
+
ReadOnlyIgnoreGridReadOnlyColumn.play = async (context) => {
|
|
346
|
+
await waitForGridReady(context);
|
|
347
|
+
const { canvasElement } = context;
|
|
348
|
+
|
|
349
|
+
const alwaysEnabledCell = canvasElement.querySelector('.ag-cell[col-id="alwaysEnabledActions"]');
|
|
350
|
+
await expect(alwaysEnabledCell).toBeInTheDocument();
|
|
351
|
+
await userEvent.click(alwaysEnabledCell as HTMLElement);
|
|
352
|
+
await expect(await screen.findByRole('menuitem', { name: 'Locate feature' })).toBeInTheDocument();
|
|
353
|
+
|
|
354
|
+
const disabledItem = await screen.findByRole('menuitem', { name: 'Other action' });
|
|
355
|
+
await expect(disabledItem).toHaveAttribute('aria-disabled', 'true');
|
|
356
|
+
await userEvent.click(disabledItem);
|
|
357
|
+
await expect(disabledMenuItemAction).not.toHaveBeenCalled();
|
|
358
|
+
|
|
359
|
+
await userEvent.keyboard('{Escape}');
|
|
360
|
+
|
|
361
|
+
const blockedCell = canvasElement.querySelector('.ag-cell[col-id="blockedActions"]');
|
|
362
|
+
await expect(blockedCell).toBeInTheDocument();
|
|
363
|
+
await userEvent.click(blockedCell as HTMLElement);
|
|
364
|
+
await expect(screen.queryByRole('menuitem', { name: 'Should not open' })).not.toBeInTheDocument();
|
|
365
|
+
};
|
|
@@ -171,3 +171,50 @@ GridFormPopoverMenuInteractions_.play = async ({ canvasElement }) => {
|
|
|
171
171
|
await userEvent.type(textArea, '{Enter}');
|
|
172
172
|
expect(updateValue).not.toHaveBeenCalled();
|
|
173
173
|
};
|
|
174
|
+
|
|
175
|
+
const AllHiddenTemplate: StoryFn<typeof GridFormPopoverMenu> = (props: GridFormPopoverMenuProps<any>) => {
|
|
176
|
+
const anchorRef = useRef<HTMLHeadingElement>(null);
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<div className={'react-menu-inline-test'}>
|
|
180
|
+
<GridContextProvider>
|
|
181
|
+
<h6 ref={anchorRef}>Interaction Test</h6>
|
|
182
|
+
<GridPopoverContext.Provider
|
|
183
|
+
value={{
|
|
184
|
+
anchorRef,
|
|
185
|
+
value: null,
|
|
186
|
+
updateValue,
|
|
187
|
+
data: { value: '' },
|
|
188
|
+
colId: '',
|
|
189
|
+
field: 'value',
|
|
190
|
+
selectedRows: [],
|
|
191
|
+
saving: false,
|
|
192
|
+
setSaving: () => {},
|
|
193
|
+
formatValue: (value) => value,
|
|
194
|
+
stopEditing: () => {},
|
|
195
|
+
}}
|
|
196
|
+
>
|
|
197
|
+
<GridFormPopoverMenu
|
|
198
|
+
{...props}
|
|
199
|
+
options={() => [
|
|
200
|
+
{ label: 'Hidden one', value: 1, action: enabledAction, hidden: true },
|
|
201
|
+
{ label: 'Hidden two', value: 2, action: enabledAction, hidden: true },
|
|
202
|
+
]}
|
|
203
|
+
/>
|
|
204
|
+
</GridPopoverContext.Provider>
|
|
205
|
+
</GridContextProvider>
|
|
206
|
+
</div>
|
|
207
|
+
);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Regression test: when every option resolves to hidden, the menu must show "No actions"
|
|
211
|
+
// rather than an empty menu body (isEmpty(options) alone doesn't catch this — the array is
|
|
212
|
+
// non-empty, just every item is hidden).
|
|
213
|
+
export const GridFormPopoverMenuAllOptionsHidden_ = AllHiddenTemplate.bind({});
|
|
214
|
+
GridFormPopoverMenuAllOptionsHidden_.play = async ({ canvasElement }) => {
|
|
215
|
+
const canvas = within(canvasElement);
|
|
216
|
+
|
|
217
|
+
expect(await canvas.findByText('No actions')).toBeInTheDocument();
|
|
218
|
+
expect(canvas.queryByRole('menuitem', { name: 'Hidden one' })).not.toBeInTheDocument();
|
|
219
|
+
expect(canvas.queryByRole('menuitem', { name: 'Hidden two' })).not.toBeInTheDocument();
|
|
220
|
+
};
|