GrafanaUI: Define tooltip or aria-label as required for IconButton (#69699)

* refactor: modify interfaces to make tooltip or aria-label required

* refactor: change functionality around aria-label and tooltip

* refactor: change and add information in storybook documentation

* refactor: remove default from tooltip

* refactor: IconButton to make tooltip or aria-label required

* refactor: Fix tests

* refactor: Fix tests

* refactor: Fix tests

* refactor: Fix tests

* feat: add migration guide for breaking change

* feat: add latest requirements to storybook docs

* refactor: separate iconbutton story with and without tooltip

* refactor: remove exported baseArgs

* refactor: clean up and restructure original story

* refactor: adjust styling

* refactor: enable control for tooltip

* refactor: clean up

* refactor: enable control for aria-label

* refactor: fix theme getting the wrong theme

* refactor: fix tests

* refactor: adjust story

* refactor: remove confusing story

* refactor: adjust controls for stories
pull/69656/head
Laura Benz 3 years ago committed by GitHub
parent 8dc9fcf88b
commit d64b6264ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 29
      docs/sources/developers/plugins/migration-guide/v10.0.x-v10.1.x/_index.md
  2. 10
      packages/grafana-ui/src/components/IconButton/IconButton.mdx
  3. 92
      packages/grafana-ui/src/components/IconButton/IconButton.story.tsx
  4. 116
      packages/grafana-ui/src/components/IconButton/IconButton.tsx
  5. 2
      packages/grafana-ui/src/components/Modal/Modal.tsx
  6. 2
      packages/grafana-ui/src/components/Select/SelectBase.test.tsx
  7. 3
      packages/grafana-ui/src/components/TagsInput/TagItem.tsx
  8. 8
      public/app/core/components/Layers/LayerDragDropList.test.tsx
  9. 2
      public/app/core/components/Layers/LayerDragDropList.tsx
  10. 4
      public/app/core/components/QueryOperationRow/QueryOperationRowHeader.test.tsx
  11. 1
      public/app/core/components/QueryOperationRow/QueryOperationRowHeader.tsx
  12. 2
      public/app/features/browse-dashboards/components/BrowseActions/DeleteModal.test.tsx
  13. 2
      public/app/features/browse-dashboards/components/BrowseActions/MoveModal.test.tsx
  14. 2
      public/app/features/browse-dashboards/components/NameCell.tsx
  15. 6
      public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.test.tsx
  16. 2
      public/app/features/query/components/QueryEditorRows.test.tsx
  17. 4
      public/app/features/query/components/QueryGroup.test.tsx
  18. 3
      public/app/features/serviceaccounts/components/ServiceAccountsListItem.tsx
  19. 4
      public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/DimensionFields.test.tsx
  20. 2
      public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filter.tsx
  21. 6
      public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filters.test.tsx
  22. 4
      public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.test.tsx

@ -0,0 +1,29 @@
---
description: Guide for migrating plugins from Grafana v10.0.x to v10.1.x
keywords:
- grafana
- plugins
- migration
- plugin
- documentation
title: Migrate plugins from Grafana 10.0.x to 10.1.x
menutitle: v10.0.x to v10.1.x
weight: 1900
---
# Migrate plugins from Grafana version 10.0.x to 10.1.x
## Accessibility update for IconButton component in grafana-ui
We updated the component's TypeScript interface due to an accessibility issue. This change was delivered to the core `grafana` repo with [PR 69699](https://github.com/grafana/grafana/pull/69699).
In case you are using the IconButton component in your plugin you will get TypeScript errors related to the change.
**Recommended actions:**
- Review use cases of IconButton in your plugin.
- Add a meaningful tooltip which the component will also use as an aria-label.
- Another option is to set an aria-label. In this case a tooltip will not be shown.
**Please note:**
The IconButton used to have a property called `ariaLabel` which got deprecated with this change. You can now use the regular property `aria-label` instead.

@ -1,17 +1,23 @@
import { Meta, ArgTypes } from '@storybook/blocks';
import { IconButton } from './IconButton';
import { Icon } from '../Icon/Icon';
import { Alert } from '../Alert/Alert';
<Meta title="MDX|IconButton" component={IconButton} />
# IconButton
This component looks just like an icon but behaves like a button. It fulfils an action when you click it and has hover and focus states. You can choose which icon size you would like to use.
This component looks just like an icon but behaves like a button. It fulfils an action when you click it and has a hover as well a focus states. You can choose which icon size you would like to use.
`IconButton` is best used when you only want an icon instead of a button with text, for example when you want to place a solitary clickable icon next to text. An example where an `IconButton` is used in Grafana is the hamburger icon at the top left which opens the new navigation.
When using `IconButton` right next to a text element consider wrapping both in a flex container and use `align-items: center;` to make them align properly.
Always keep in mind to add text for a tooltip and an aria label.
There are two options to use the IconButton:
- with `Tooltip`: This is the preferred option since we don't want to rely on assumptions when it comes to the meaning an `Icon` has. Add a text for the `Tooltip`. It will be used for the `aria-label` as well.
- without `Tooltip`: This is an option for use cases where the `Icon` is unambiguous e.g <Icon name="angle-down" /> for expanding a folder. Add a text for the `aria-label` and there will **not** be a `Tooltip`.
The IconButton used to have a property called `ariaLabel` which got deprecated. You can now use the regular property `aria-label` instead.
<Alert severity="warning" title={'Please note:'}>
After reviewing this component we would like you to know that there are only 5 sizes available (sizes xs to xl). Sizes

@ -7,13 +7,16 @@ import { IconSize, IconName } from '../../types';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { HorizontalGroup, VerticalGroup } from '../Layout/Layout';
import { IconButton, IconButtonVariant, Props as IconButtonProps } from './IconButton';
import { BasePropsWithTooltip, IconButton, IconButtonVariant, Props as IconButtonProps } from './IconButton';
import mdx from './IconButton.mdx';
interface ScenarioProps {
background: 'canvas' | 'primary' | 'secondary';
}
const defaultExcludes = ['ariaLabel', 'aria-label'];
const additionalExcludes = ['size', 'name', 'variant', 'iconType'];
const meta: Meta<typeof IconButton> = {
title: 'Buttons/IconButton',
component: IconButton,
@ -22,6 +25,7 @@ const meta: Meta<typeof IconButton> = {
docs: {
page: mdx,
},
controls: { exclude: defaultExcludes },
},
args: {
name: 'apps',
@ -30,7 +34,8 @@ const meta: Meta<typeof IconButton> = {
tooltip: 'sample tooltip message',
tooltipPlacement: 'top',
variant: 'secondary',
ariaLabel: 'sample aria-label content',
ariaLabel: 'this property is deprecated',
['aria-label']: 'sample aria-label content',
},
argTypes: {
tooltip: {
@ -43,7 +48,7 @@ export const Basic: StoryFn<typeof IconButton> = (args: IconButtonProps) => {
return <IconButton {...args} />;
};
export const ExamplesSizes = () => {
export const ExamplesSizes = (args: BasePropsWithTooltip) => {
const theme = useTheme2();
const sizes: IconSize[] = ['xs', 'sm', 'md', 'lg', 'xl'];
const icons: IconName[] = ['search', 'trash-alt', 'arrow-left', 'times'];
@ -56,17 +61,22 @@ export const ExamplesSizes = () => {
`;
return (
<HorizontalGroup spacing="md">
<HorizontalGroup justify="center">
{variants.map((variant) => {
return (
<div key={variant}>
<div
key={variant}
className={css`
margin: auto ${theme.spacing(1)};
`}
>
<p>{variant}</p>
{icons.map((icon) => {
return (
<div className={rowStyle} key={icon}>
{sizes.map((size) => (
<span key={icon + size}>
<IconButton name={icon} size={size} variant={variant} tooltip="Tooltip example" />
<IconButton name={icon} size={size} variant={variant} tooltip={args.tooltip} />
</span>
))}
</div>
@ -81,7 +91,7 @@ export const ExamplesSizes = () => {
<div className={rowStyle} key={icon}>
{sizes.map((size) => (
<span key={icon + size}>
<IconButton name={icon} size={size} tooltip="Tooltip example" disabled />
<IconButton name={icon} size={size} tooltip={args.tooltip} disabled />
</span>
))}
</div>
@ -91,7 +101,42 @@ export const ExamplesSizes = () => {
);
};
export const ExamplesBackground = () => {
ExamplesSizes.parameters = {
controls: {
exclude: [...defaultExcludes, ...additionalExcludes],
},
};
export const ExamplesBackground = (args: BasePropsWithTooltip) => {
const RenderBackgroundScenario = ({ background }: ScenarioProps) => {
const theme = useTheme2();
const variants: IconButtonVariant[] = ['primary', 'secondary', 'destructive'];
return (
<div
className={css`
padding: 30px;
background: ${theme.colors.background[background]};
`}
>
<VerticalGroup spacing="md">
<div>{background}</div>
<div
className={css`
display: flex;
gap: ${theme.spacing(2)};
`}
>
{variants.map((variant) => {
return <IconButton name="times" size="xl" variant={variant} key={variant} tooltip={args.tooltip} />;
})}
<IconButton name="times" size="xl" tooltip={args.tooltip} disabled />
</div>
</VerticalGroup>
</div>
);
};
return (
<div>
<RenderBackgroundScenario background="canvas" />
@ -101,33 +146,10 @@ export const ExamplesBackground = () => {
);
};
const RenderBackgroundScenario = ({ background }: ScenarioProps) => {
const theme = useTheme2();
const variants: IconButtonVariant[] = ['primary', 'secondary', 'destructive'];
return (
<div
className={css`
padding: 30px;
background: ${theme.colors.background[background]};
`}
>
<VerticalGroup spacing="md">
<div>{background}</div>
<div
className={css`
display: flex;
gap: ${theme.spacing(2)};
`}
>
{variants.map((variant) => {
return <IconButton name="times" size="xl" variant={variant} key={variant} tooltip="Tooltip example" />;
})}
<IconButton name="times" size="xl" tooltip="Tooltip example" disabled />
</div>
</VerticalGroup>
</div>
);
ExamplesBackground.parameters = {
controls: {
exclude: [...defaultExcludes, ...additionalExcludes],
},
};
export default meta;

@ -15,76 +15,92 @@ export type IconButtonVariant = 'primary' | 'secondary' | 'destructive';
type LimitedIconSize = ComponentSize | 'xl';
export interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {
interface BaseProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'aria-label'> {
/** Name of the icon **/
name: IconName;
/** Icon size - sizes xxl and xxxl are deprecated and when used being decreased to xl*/
size?: IconSize;
/** Type of the icon - mono or default */
iconType?: IconType;
/** Tooltip content to display on hover */
tooltip?: PopoverContent;
/** Position of the tooltip */
tooltipPlacement?: TooltipPlacement;
/** Variant to change the color of the Icon */
variant?: IconButtonVariant;
/** Text available only for screen readers. Will use tooltip text as fallback. */
}
export interface BasePropsWithTooltip extends BaseProps {
/** Tooltip content to display on hover and as the aria-label */
tooltip: PopoverContent;
/** Position of the tooltip */
tooltipPlacement?: TooltipPlacement;
}
interface BasePropsWithAriaLabel extends BaseProps {
/** @deprecated use aria-label instead*/
ariaLabel?: string;
/** Text available only for screen readers. No tooltip will be set in this case. */
['aria-label']: string;
}
export const IconButton = React.forwardRef<HTMLButtonElement, Props>(
(
{
name,
size = 'md',
iconType,
tooltip,
tooltipPlacement,
ariaLabel,
className,
variant = 'secondary',
...restProps
},
ref
) => {
const theme = useTheme2();
let limitedIconSize: LimitedIconSize;
// very large icons (xl to xxxl) are unified to size xl
if (size === 'xxl' || size === 'xxxl') {
deprecationWarning('IconButton', 'size="xxl" and size="xxxl"', 'size="xl"');
limitedIconSize = 'xl';
} else {
limitedIconSize = size;
}
const styles = getStyles(theme, limitedIconSize, variant);
const tooltipString = typeof tooltip === 'string' ? tooltip : '';
// When using tooltip, ref is forwarded to Tooltip component instead for https://github.com/grafana/grafana/issues/65632
const button = (
export type Props = BasePropsWithTooltip | BasePropsWithAriaLabel;
export const IconButton = React.forwardRef<HTMLButtonElement, Props>((props, ref) => {
const { size = 'md', variant = 'secondary' } = props;
const theme = useTheme2();
let limitedIconSize: LimitedIconSize;
// very large icons (xl to xxxl) are unified to size xl
if (size === 'xxl' || size === 'xxxl') {
deprecationWarning('IconButton', 'size="xxl" and size="xxxl"', 'size="xl"');
limitedIconSize = 'xl';
} else {
limitedIconSize = size;
}
const styles = getStyles(theme, limitedIconSize, variant);
let ariaLabel: string | undefined;
let buttonRef: typeof ref | undefined;
if ('tooltip' in props) {
const { tooltip } = props;
ariaLabel = typeof tooltip === 'string' ? tooltip : undefined;
} else if ('ariaLabel' in props || 'aria-label' in props) {
const { ariaLabel: deprecatedAriaLabel, ['aria-label']: ariaLabelProp } = props;
ariaLabel = ariaLabelProp || deprecatedAriaLabel;
buttonRef = ref;
}
// When using tooltip, ref is forwarded to Tooltip component instead for https://github.com/grafana/grafana/issues/65632
if ('tooltip' in props) {
const { name, iconType, className, tooltip, tooltipPlacement, ...restProps } = props;
return (
<Tooltip ref={ref} content={tooltip} placement={tooltipPlacement}>
<button
{...restProps}
ref={buttonRef}
aria-label={ariaLabel}
className={cx(styles.button, className)}
type="button"
>
<Icon name={name} size={limitedIconSize} className={styles.icon} type={iconType} />
</button>
</Tooltip>
);
} else {
const { name, iconType, className, ...restProps } = props;
return (
<button
ref={tooltip ? undefined : ref}
aria-label={ariaLabel || tooltipString}
{...restProps}
ref={buttonRef}
aria-label={ariaLabel}
className={cx(styles.button, className)}
type="button"
>
<Icon name={name} size={limitedIconSize} className={styles.icon} type={iconType} />
</button>
);
if (tooltip) {
return (
<Tooltip ref={ref} content={tooltip} placement={tooltipPlacement}>
{button}
</Tooltip>
);
}
return button;
}
);
});
IconButton.displayName = 'IconButton';

@ -84,7 +84,7 @@ export function Modal(props: PropsWithChildren<Props>) {
typeof title !== 'string' && title
}
<div className={styles.modalHeaderClose}>
<IconButton aria-label="Close dialog" name="times" size="xl" onClick={onDismiss} tooltip="Close" />
<IconButton name="times" size="xl" onClick={onDismiss} tooltip="Close" />
</div>
</div>
<div className={cx(styles.modalContent, contentClassName)}>{children}</div>

@ -233,7 +233,7 @@ describe('SelectBase', () => {
expect(screen.getByLabelText('My select')).toBeInTheDocument();
await userEvent.click(screen.getByLabelText('Remove Option 1'));
await userEvent.click(screen.getAllByLabelText('Remove')[0]);
expect(onChangeHandler).toHaveBeenCalledWith([], {
action: 'remove-value',
name: undefined,

@ -28,8 +28,7 @@ export const TagItem = ({ name, disabled, onRemove }: Props) => {
name="times"
size="lg"
disabled={disabled}
ariaLabel={`Remove "${name}" tag`}
tooltip="Remove tag"
tooltip={`Remove "${name}" tag`}
onClick={() => onRemove(name)}
className={styles.buttonStyles}
/>

@ -25,15 +25,15 @@ describe('LayerDragDropList', () => {
it('showActions', () => {
renderScenario({ showActions: () => true });
expect(screen.getAllByLabelText('Duplicate button').length).toEqual(2);
expect(screen.getAllByLabelText('Remove button').length).toEqual(2);
expect(screen.getAllByLabelText('Duplicate').length).toEqual(2);
expect(screen.getAllByLabelText('Remove').length).toEqual(2);
});
it('showActions - no duplicate', () => {
renderScenario({ showActions: () => true, onDuplicate: undefined });
expect(screen.getAllByLabelText('Remove button').length).toEqual(2);
expect(screen.queryAllByLabelText('Duplicate button').length).toEqual(0);
expect(screen.getAllByLabelText('Remove').length).toEqual(2);
expect(screen.queryAllByLabelText('Duplicate').length).toEqual(0);
});
it('renders draggable icon', () => {

@ -81,7 +81,6 @@ export const LayerDragDropList = <T extends LayerElement>({
<IconButton
name="copy"
tooltip="Duplicate"
ariaLabel="Duplicate button"
className={style.actionIcon}
onClick={() => onDuplicate(element)}
/>
@ -90,7 +89,6 @@ export const LayerDragDropList = <T extends LayerElement>({
<IconButton
name="trash-alt"
tooltip="Remove"
ariaLabel="Remove button"
className={cx(style.actionIcon, style.dragIcon)}
onClick={() => onDelete(element)}
/>

@ -24,11 +24,11 @@ describe('QueryOperationRowHeader', () => {
describe('collapsable property', () => {
test('should show the button to collapse the query row by default', () => {
setup();
expect(screen.getByLabelText('toggle collapse and expand query row')).toBeInTheDocument();
expect(screen.getByLabelText('Collapse query row')).toBeInTheDocument();
});
test('should hide the button to collapse the query row when collapsable is set as false', () => {
setup({ collapsable: false });
expect(screen.queryByLabelText('toggle collapse and expand query row')).not.toBeInTheDocument();
expect(screen.queryByLabelText('Collapse query row')).not.toBeInTheDocument();
});
});
});

@ -40,7 +40,6 @@ export const QueryOperationRowHeader = ({
{collapsable && (
<IconButton
name={isContentVisible ? 'angle-down' : 'angle-right'}
aria-label="toggle collapse and expand query row"
tooltip={isContentVisible ? 'Collapse query row' : 'Expand query row'}
className={styles.collapseIcon}
onClick={onRowToggle}

@ -72,7 +72,7 @@ describe('browse-dashboards DeleteModal', () => {
it('calls onDismiss when clicking the X', async () => {
render(<DeleteModal {...defaultProps} />);
await userEvent.click(await screen.findByRole('button', { name: 'Close dialog' }));
await userEvent.click(await screen.findByRole('button', { name: 'Close' }));
expect(mockOnDismiss).toHaveBeenCalled();
});
});

@ -113,7 +113,7 @@ describe('browse-dashboards MoveModal', () => {
it('calls onDismiss when clicking the X', async () => {
render(<MoveModal {...props} />);
await userEvent.click(await screen.findByRole('button', { name: 'Close dialog' }));
await userEvent.click(await screen.findByRole('button', { name: 'Close' }));
expect(mockOnDismiss).toHaveBeenCalled();
});
});

@ -74,7 +74,7 @@ export function NameCell({ row: { original: data }, onFolderClick }: NameCellPro
onFolderClick(item.uid, !isOpen);
}}
name={isOpen ? 'angle-down' : 'angle-right'}
ariaLabel={isOpen ? 'Collapse folder' : 'Expand folder'}
tooltip={isOpen ? 'Collapse folder' : 'Expand folder'}
/>
)}
</>

@ -318,7 +318,7 @@ describe('LibraryPanelsSearch', () => {
expect(card()).toBeInTheDocument();
expect(within(card()).getByText(/library panel name/i)).toBeInTheDocument();
expect(within(card()).getByText(/library panel description/i)).toBeInTheDocument();
expect(within(card()).getByLabelText(/delete button on panel type card/i)).toBeInTheDocument();
expect(within(card()).getByLabelText(/Delete/i)).toBeInTheDocument();
});
});
@ -354,9 +354,9 @@ describe('LibraryPanelsSearch', () => {
}
);
await userEvent.click(screen.getByLabelText(/delete button on panel type card/i));
await userEvent.click(screen.getByLabelText('Delete'));
await waitFor(() => expect(screen.getByText('Do you want to delete this panel?')).toBeInTheDocument());
await userEvent.click(screen.getByRole('button', { name: 'Delete' }));
await userEvent.click(screen.getAllByRole('button', { name: 'Delete' })[1]);
await waitFor(() => {
expect(getLibraryPanelsSpy).toHaveBeenCalledWith({

@ -96,7 +96,7 @@ describe('QueryEditorRows', () => {
const queryEditorRows = await screen.findAllByTestId('query-editor-row');
for (const childQuery of queryEditorRows) {
const toggleExpandButton = queryByLabelText(childQuery, 'toggle collapse and expand query row') as HTMLElement;
const toggleExpandButton = queryByLabelText(childQuery, 'Collapse query row') as HTMLElement;
expect(toggleExpandButton).toBeInTheDocument();
expect(toggleExpandButton.getAttribute('aria-expanded')).toBe('true');

@ -80,7 +80,7 @@ describe('QueryGroup', () => {
await userEvent.click(addExpressionButton);
const lastQueryEditorRow = (await screen.findAllByTestId('query-editor-row')).at(-1);
const lastEditorToggleRow = (await screen.findAllByLabelText('toggle collapse and expand query row')).at(-1);
const lastEditorToggleRow = (await screen.findAllByLabelText('Collapse query row')).at(-1);
expect(lastEditorToggleRow?.getAttribute('aria-expanded')).toBe('true');
expect(lastQueryEditorRow?.firstElementChild?.children.length).toBe(2);
@ -97,7 +97,7 @@ describe('QueryGroup', () => {
await userEvent.click(addQueryButton);
const lastQueryEditorRow = (await screen.findAllByTestId('query-editor-row')).at(-1);
const lastEditorToggleRow = (await screen.findAllByLabelText('toggle collapse and expand query row')).at(-1);
const lastEditorToggleRow = (await screen.findAllByLabelText('Collapse query row')).at(-1);
expect(lastEditorToggleRow?.getAttribute('aria-expanded')).toBe('true');
expect(lastQueryEditorRow?.firstElementChild?.children.length).toBe(2);

@ -132,8 +132,7 @@ const ServiceAccountListItem = memo(
name="trash-alt"
size="md"
onClick={() => onRemoveButtonClick(serviceAccount)}
aria-label={`Delete service account ${serviceAccount.name}`}
tooltip="Delete account"
tooltip={`Delete service account ${serviceAccount.name}`}
/>
)}
</HorizontalGroup>

@ -201,8 +201,8 @@ describe(`Azure Monitor QueryEditor`, () => {
/>
);
await screen.findByText('testlabel');
const labelClear = await screen.findByLabelText('Remove testlabel');
await user.click(labelClear);
const labelClear = await screen.findAllByLabelText('Remove');
await user.click(labelClear[0]);
mockQuery = setDimensionFilterValue(mockQuery, 0, 'filters', []);
expect(onQueryChange).toHaveBeenCalledWith({
...mockQuery,

@ -270,7 +270,7 @@ const Filter = (
onCloseMenu={() => onFieldChange('filters', item, selected, onChange)}
hideSelectedOptions={false}
/>
<AccessoryButton aria-label="Remove" icon="times" variant="secondary" onClick={onDelete} type="button" />
<AccessoryButton aria-label="Remove filter" icon="times" variant="secondary" onClick={onDelete} type="button" />
</HorizontalGroup>
);
};

@ -281,7 +281,7 @@ describe(`Traces Filters`, () => {
rerender
);
const removeButtons = screen.getAllByLabelText('Remove');
const removeButtons = screen.getAllByLabelText('Remove filter');
mockQuery = {
...mockQuery,
@ -388,9 +388,9 @@ describe(`Traces Filters`, () => {
],
},
};
const removeLabel = screen.getByLabelText(`Remove test-app-id-2`);
const removeLabel = screen.getAllByLabelText(`Remove`);
await act(async () => {
await userEvent.click(removeLabel);
await userEvent.click(removeLabel[1]);
});
rerender(

@ -112,8 +112,8 @@ describe('SearchField', () => {
expect(updateFilter).toHaveBeenCalledWith({ ...filter, value: ['driver', 'customer'] });
// Remove the first value
const firstValRemove = await screen.findByLabelText('Remove driver');
await user.click(firstValRemove);
const firstValRemove = await screen.findAllByLabelText('Remove');
await user.click(firstValRemove[0]);
expect(updateFilter).toHaveBeenCalledWith({ ...filter, value: ['customer'] });
}
});

Loading…
Cancel
Save