mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[Controls] Bulk select for options list control (#221010)
This commit is contained in:
parent
48e4ede08a
commit
749aeb70e9
7 changed files with 288 additions and 23 deletions
|
@ -0,0 +1,133 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the "Elastic License
|
||||||
|
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||||
|
* Public License v 1"; you may not use this file except in compliance with, at
|
||||||
|
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||||
|
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { take } from 'lodash';
|
||||||
|
import { getOptionsListContextMock } from '../../mocks/api_mocks';
|
||||||
|
import { OptionsListControlContext } from '../options_list_context_provider';
|
||||||
|
import type { OptionsListComponentApi } from '../types';
|
||||||
|
import { OptionsListPopoverActionBar } from './options_list_popover_action_bar';
|
||||||
|
import { OptionsListDisplaySettings } from '../../../../../common/options_list';
|
||||||
|
import { MAX_OPTIONS_LIST_BULK_SELECT_SIZE } from '../constants';
|
||||||
|
|
||||||
|
const allOptions = [
|
||||||
|
{ value: 'moo', docCount: 1 },
|
||||||
|
{ value: 'tweet', docCount: 2 },
|
||||||
|
{ value: 'oink', docCount: 3 },
|
||||||
|
{ value: 'bark', docCount: 4 },
|
||||||
|
{ value: 'meow', docCount: 5 },
|
||||||
|
{ value: 'woof', docCount: 6 },
|
||||||
|
{ value: 'roar', docCount: 7 },
|
||||||
|
{ value: 'honk', docCount: 8 },
|
||||||
|
{ value: 'beep', docCount: 9 },
|
||||||
|
{ value: 'chirp', docCount: 10 },
|
||||||
|
{ value: 'baa', docCount: 11 },
|
||||||
|
{ value: 'toot', docCount: 11 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const renderComponent = ({
|
||||||
|
componentApi,
|
||||||
|
displaySettings,
|
||||||
|
showOnlySelected,
|
||||||
|
}: {
|
||||||
|
componentApi: OptionsListComponentApi;
|
||||||
|
displaySettings: OptionsListDisplaySettings;
|
||||||
|
showOnlySelected?: boolean;
|
||||||
|
}) => {
|
||||||
|
return render(
|
||||||
|
<OptionsListControlContext.Provider
|
||||||
|
value={{
|
||||||
|
componentApi,
|
||||||
|
displaySettings,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<OptionsListPopoverActionBar
|
||||||
|
showOnlySelected={showOnlySelected ?? false}
|
||||||
|
setShowOnlySelected={() => {}}
|
||||||
|
/>
|
||||||
|
</OptionsListControlContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSelectAllCheckbox = () => screen.getByRole('checkbox', { name: /Select all/i });
|
||||||
|
|
||||||
|
const getSearchInput = () => screen.getByRole('searchbox', { name: /Filter suggestions/i });
|
||||||
|
|
||||||
|
describe('Options list popover', () => {
|
||||||
|
test('displays search input', async () => {
|
||||||
|
const contextMock = getOptionsListContextMock();
|
||||||
|
contextMock.componentApi.setTotalCardinality(allOptions.length);
|
||||||
|
contextMock.componentApi.setAvailableOptions(take(allOptions, 5));
|
||||||
|
contextMock.componentApi.setSearchString('moo');
|
||||||
|
renderComponent(contextMock);
|
||||||
|
|
||||||
|
expect(getSearchInput()).toBeEnabled();
|
||||||
|
expect(getSearchInput()).toHaveValue('moo');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('displays total cardinality for available options', async () => {
|
||||||
|
const contextMock = getOptionsListContextMock();
|
||||||
|
contextMock.componentApi.setTotalCardinality(allOptions.length);
|
||||||
|
contextMock.componentApi.setAvailableOptions(take(allOptions, 5));
|
||||||
|
renderComponent(contextMock);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('optionsList-cardinality-label')).toHaveTextContent(
|
||||||
|
allOptions.length.toString()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('displays "Select all" checkbox next to total cardinality', async () => {
|
||||||
|
const contextMock = getOptionsListContextMock();
|
||||||
|
contextMock.componentApi.setTotalCardinality(80);
|
||||||
|
contextMock.componentApi.setAvailableOptions(take(allOptions, 10));
|
||||||
|
renderComponent(contextMock);
|
||||||
|
|
||||||
|
expect(getSelectAllCheckbox()).toBeEnabled();
|
||||||
|
expect(getSelectAllCheckbox()).not.toBeChecked();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Select all is checked when all available options are selected ', async () => {
|
||||||
|
const contextMock = getOptionsListContextMock();
|
||||||
|
contextMock.componentApi.setTotalCardinality(80);
|
||||||
|
contextMock.componentApi.setAvailableOptions([{ value: 'moo', docCount: 1 }]);
|
||||||
|
contextMock.componentApi.setSelectedOptions(['moo']);
|
||||||
|
renderComponent(contextMock);
|
||||||
|
|
||||||
|
expect(getSelectAllCheckbox()).toBeEnabled();
|
||||||
|
expect(getSelectAllCheckbox()).toBeChecked();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('bulk selections are disabled when there are more than 100 available options', async () => {
|
||||||
|
const contextMock = getOptionsListContextMock();
|
||||||
|
contextMock.componentApi.setTotalCardinality(MAX_OPTIONS_LIST_BULK_SELECT_SIZE + 1);
|
||||||
|
contextMock.componentApi.setAvailableOptions(take(allOptions, 10));
|
||||||
|
renderComponent(contextMock);
|
||||||
|
|
||||||
|
expect(getSelectAllCheckbox()).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('bulk selections are disabled when there are no available options', async () => {
|
||||||
|
const contextMock = getOptionsListContextMock();
|
||||||
|
contextMock.componentApi.setTotalCardinality(0);
|
||||||
|
contextMock.componentApi.setAvailableOptions([]);
|
||||||
|
renderComponent(contextMock);
|
||||||
|
|
||||||
|
expect(getSelectAllCheckbox()).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('bulk selections are disabled when showOnlySelected is true', async () => {
|
||||||
|
const contextMock = getOptionsListContextMock();
|
||||||
|
contextMock.componentApi.setTotalCardinality(0);
|
||||||
|
contextMock.componentApi.setAvailableOptions([]);
|
||||||
|
renderComponent({ ...contextMock, showOnlySelected: true });
|
||||||
|
|
||||||
|
expect(getSelectAllCheckbox()).toBeDisabled();
|
||||||
|
});
|
||||||
|
});
|
|
@ -7,10 +7,11 @@
|
||||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useMemo } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EuiButtonIcon,
|
EuiButtonIcon,
|
||||||
|
EuiCheckbox,
|
||||||
EuiFieldSearch,
|
EuiFieldSearch,
|
||||||
EuiFlexGroup,
|
EuiFlexGroup,
|
||||||
EuiFlexItem,
|
EuiFlexItem,
|
||||||
|
@ -23,12 +24,16 @@ import {
|
||||||
useBatchedPublishingSubjects,
|
useBatchedPublishingSubjects,
|
||||||
useStateFromPublishingSubject,
|
useStateFromPublishingSubject,
|
||||||
} from '@kbn/presentation-publishing';
|
} from '@kbn/presentation-publishing';
|
||||||
|
|
||||||
|
import { lastValueFrom, take } from 'rxjs';
|
||||||
import { css } from '@emotion/react';
|
import { css } from '@emotion/react';
|
||||||
import { useMemoCss } from '@kbn/css-utils/public/use_memo_css';
|
import { useMemoCss } from '@kbn/css-utils/public/use_memo_css';
|
||||||
|
import { OptionsListSuggestions } from '../../../../../common/options_list';
|
||||||
import { getCompatibleSearchTechniques } from '../../../../../common/options_list/suggestions_searching';
|
import { getCompatibleSearchTechniques } from '../../../../../common/options_list/suggestions_searching';
|
||||||
import { useOptionsListContext } from '../options_list_context_provider';
|
import { useOptionsListContext } from '../options_list_context_provider';
|
||||||
import { OptionsListPopoverSortingButton } from './options_list_popover_sorting_button';
|
import { OptionsListPopoverSortingButton } from './options_list_popover_sorting_button';
|
||||||
import { OptionsListStrings } from '../options_list_strings';
|
import { OptionsListStrings } from '../options_list_strings';
|
||||||
|
import { MAX_OPTIONS_LIST_BULK_SELECT_SIZE, MAX_OPTIONS_LIST_REQUEST_SIZE } from '../constants';
|
||||||
|
|
||||||
interface OptionsListPopoverProps {
|
interface OptionsListPopoverProps {
|
||||||
showOnlySelected: boolean;
|
showOnlySelected: boolean;
|
||||||
|
@ -51,6 +56,15 @@ const optionsListPopoverStyles = {
|
||||||
height: ${euiTheme.size.base};
|
height: ${euiTheme.size.base};
|
||||||
border-right: ${euiTheme.border.thin};
|
border-right: ${euiTheme.border.thin};
|
||||||
`,
|
`,
|
||||||
|
selectAllCheckbox: ({ euiTheme }: UseEuiTheme) => css`
|
||||||
|
.euiCheckbox__square {
|
||||||
|
margin-block-start: 0;
|
||||||
|
}
|
||||||
|
.euiCheckbox__label {
|
||||||
|
align-items: center;
|
||||||
|
padding-inline-start: ${euiTheme.size.xs};
|
||||||
|
}
|
||||||
|
`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const OptionsListPopoverActionBar = ({
|
export const OptionsListPopoverActionBar = ({
|
||||||
|
@ -58,6 +72,7 @@ export const OptionsListPopoverActionBar = ({
|
||||||
setShowOnlySelected,
|
setShowOnlySelected,
|
||||||
}: OptionsListPopoverProps) => {
|
}: OptionsListPopoverProps) => {
|
||||||
const { componentApi, displaySettings } = useOptionsListContext();
|
const { componentApi, displaySettings } = useOptionsListContext();
|
||||||
|
const [areAllSelected, setAllSelected] = useState<boolean>(false);
|
||||||
|
|
||||||
// Using useStateFromPublishingSubject instead of useBatchedPublishingSubjects
|
// Using useStateFromPublishingSubject instead of useBatchedPublishingSubjects
|
||||||
// to avoid debouncing input value
|
// to avoid debouncing input value
|
||||||
|
@ -66,17 +81,23 @@ export const OptionsListPopoverActionBar = ({
|
||||||
const [
|
const [
|
||||||
searchTechnique,
|
searchTechnique,
|
||||||
searchStringValid,
|
searchStringValid,
|
||||||
invalidSelections,
|
selectedOptions = [],
|
||||||
totalCardinality,
|
totalCardinality,
|
||||||
field,
|
field,
|
||||||
|
fieldName,
|
||||||
allowExpensiveQueries,
|
allowExpensiveQueries,
|
||||||
|
availableOptions = [],
|
||||||
|
dataLoading,
|
||||||
] = useBatchedPublishingSubjects(
|
] = useBatchedPublishingSubjects(
|
||||||
componentApi.searchTechnique$,
|
componentApi.searchTechnique$,
|
||||||
componentApi.searchStringValid$,
|
componentApi.searchStringValid$,
|
||||||
componentApi.invalidSelections$,
|
componentApi.selectedOptions$,
|
||||||
componentApi.totalCardinality$,
|
componentApi.totalCardinality$,
|
||||||
componentApi.field$,
|
componentApi.field$,
|
||||||
componentApi.parentApi.allowExpensiveQueries$
|
componentApi.fieldName$,
|
||||||
|
componentApi.parentApi.allowExpensiveQueries$,
|
||||||
|
componentApi.availableOptions$,
|
||||||
|
componentApi.dataLoading$
|
||||||
);
|
);
|
||||||
|
|
||||||
const compatibleSearchTechniques = useMemo(() => {
|
const compatibleSearchTechniques = useMemo(() => {
|
||||||
|
@ -89,6 +110,42 @@ export const OptionsListPopoverActionBar = ({
|
||||||
[searchTechnique, compatibleSearchTechniques]
|
[searchTechnique, compatibleSearchTechniques]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const loadMoreOptions = useCallback(async (): Promise<OptionsListSuggestions | undefined> => {
|
||||||
|
componentApi.setRequestSize(Math.min(totalCardinality, MAX_OPTIONS_LIST_REQUEST_SIZE));
|
||||||
|
componentApi.loadMoreSubject.next(); // trigger refetch with loadMoreSubject
|
||||||
|
return lastValueFrom(componentApi.availableOptions$.pipe(take(2)));
|
||||||
|
}, [componentApi, totalCardinality]);
|
||||||
|
|
||||||
|
const hasNoOptions = availableOptions.length < 1;
|
||||||
|
const hasTooManyOptions = showOnlySelected
|
||||||
|
? selectedOptions.length > MAX_OPTIONS_LIST_BULK_SELECT_SIZE
|
||||||
|
: totalCardinality > MAX_OPTIONS_LIST_BULK_SELECT_SIZE;
|
||||||
|
|
||||||
|
const isBulkSelectDisabled = dataLoading || hasNoOptions || hasTooManyOptions || showOnlySelected;
|
||||||
|
|
||||||
|
const handleBulkAction = useCallback(
|
||||||
|
async (bulkAction: (keys: string[]) => void) => {
|
||||||
|
bulkAction(availableOptions.map(({ value }) => value as string));
|
||||||
|
|
||||||
|
if (totalCardinality > availableOptions.length) {
|
||||||
|
const newAvailableOptions = (await loadMoreOptions()) ?? [];
|
||||||
|
bulkAction(newAvailableOptions.map(({ value }) => value as string));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[availableOptions, loadMoreOptions, totalCardinality]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (availableOptions.some(({ value }) => !selectedOptions.includes(value as string))) {
|
||||||
|
if (areAllSelected) {
|
||||||
|
setAllSelected(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!areAllSelected) {
|
||||||
|
setAllSelected(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [availableOptions, selectedOptions, areAllSelected]);
|
||||||
const styles = useMemoCss(optionsListPopoverStyles);
|
const styles = useMemoCss(optionsListPopoverStyles);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -108,6 +165,7 @@ export const OptionsListPopoverActionBar = ({
|
||||||
placeholder={OptionsListStrings.popover.getSearchPlaceholder(
|
placeholder={OptionsListStrings.popover.getSearchPlaceholder(
|
||||||
allowExpensiveQueries ? defaultSearchTechnique : 'exact'
|
allowExpensiveQueries ? defaultSearchTechnique : 'exact'
|
||||||
)}
|
)}
|
||||||
|
aria-label={OptionsListStrings.popover.getSearchAriaLabel(fieldName)}
|
||||||
/>
|
/>
|
||||||
</EuiFormRow>
|
</EuiFormRow>
|
||||||
)}
|
)}
|
||||||
|
@ -119,26 +177,49 @@ export const OptionsListPopoverActionBar = ({
|
||||||
responsive={false}
|
responsive={false}
|
||||||
>
|
>
|
||||||
{allowExpensiveQueries && (
|
{allowExpensiveQueries && (
|
||||||
<EuiFlexItem grow={false}>
|
|
||||||
<EuiText size="xs" color="subdued" data-test-subj="optionsList-cardinality-label">
|
|
||||||
{OptionsListStrings.popover.getCardinalityLabel(totalCardinality)}
|
|
||||||
</EuiText>
|
|
||||||
</EuiFlexItem>
|
|
||||||
)}
|
|
||||||
{invalidSelections && invalidSelections.size > 0 && (
|
|
||||||
<>
|
<>
|
||||||
{allowExpensiveQueries && (
|
|
||||||
<EuiFlexItem grow={false}>
|
|
||||||
<div css={styles.borderDiv} />
|
|
||||||
</EuiFlexItem>
|
|
||||||
)}
|
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
<EuiText size="xs" color="subdued">
|
<EuiText size="xs" color="subdued" data-test-subj="optionsList-cardinality-label">
|
||||||
{OptionsListStrings.popover.getInvalidSelectionsLabel(invalidSelections.size)}
|
{OptionsListStrings.popover.getCardinalityLabel(totalCardinality)}
|
||||||
</EuiText>
|
</EuiText>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<div css={styles.borderDiv} />
|
||||||
|
</EuiFlexItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiToolTip
|
||||||
|
content={
|
||||||
|
hasTooManyOptions
|
||||||
|
? OptionsListStrings.popover.getMaximumBulkSelectionTooltip()
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<EuiCheckbox
|
||||||
|
checked={areAllSelected}
|
||||||
|
id={`optionsList-control-selectAll-checkbox-${componentApi.uuid}`}
|
||||||
|
// indeterminate={selectedOptions.length > 0 && !areAllSelected}
|
||||||
|
disabled={isBulkSelectDisabled}
|
||||||
|
data-test-subj="optionsList-control-selectAll"
|
||||||
|
onChange={() => {
|
||||||
|
if (areAllSelected) {
|
||||||
|
handleBulkAction(componentApi.deselectAll);
|
||||||
|
setAllSelected(false);
|
||||||
|
} else {
|
||||||
|
handleBulkAction(componentApi.selectAll);
|
||||||
|
setAllSelected(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
css={styles.selectAllCheckbox}
|
||||||
|
label={
|
||||||
|
<EuiText size="xs">
|
||||||
|
{OptionsListStrings.popover.getSelectAllButtonLabel()}
|
||||||
|
</EuiText>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</EuiToolTip>
|
||||||
|
</EuiFlexItem>
|
||||||
<EuiFlexItem grow={true}>
|
<EuiFlexItem grow={true}>
|
||||||
<EuiFlexGroup
|
<EuiFlexGroup
|
||||||
gutterSize="xs"
|
gutterSize="xs"
|
||||||
|
|
|
@ -57,6 +57,11 @@ export const OptionsListPopoverInvalidSelections = () => {
|
||||||
key: String(key),
|
key: String(key),
|
||||||
label: fieldFormatter(key),
|
label: fieldFormatter(key),
|
||||||
checked: 'on',
|
checked: 'on',
|
||||||
|
css: css`
|
||||||
|
.euiSelectableListItem__prepend {
|
||||||
|
margin-inline-end: 0;
|
||||||
|
}
|
||||||
|
`,
|
||||||
className: 'optionsList__selectionInvalid',
|
className: 'optionsList__selectionInvalid',
|
||||||
'data-test-subj': `optionsList-control-invalid-selection-${key}`,
|
'data-test-subj': `optionsList-control-invalid-selection-${key}`,
|
||||||
prepend: (
|
prepend: (
|
||||||
|
@ -76,7 +81,7 @@ export const OptionsListPopoverInvalidSelections = () => {
|
||||||
<>
|
<>
|
||||||
<EuiSpacer size="s" />
|
<EuiSpacer size="s" />
|
||||||
<EuiTitle size="xxs" data-test-subj="optionList__invalidSelectionLabel" css={styles.title}>
|
<EuiTitle size="xxs" data-test-subj="optionList__invalidSelectionLabel" css={styles.title}>
|
||||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
<EuiFlexGroup gutterSize="s" alignItems="center" justifyContent="flexStart">
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
<EuiIcon
|
<EuiIcon
|
||||||
type="warning"
|
type="warning"
|
||||||
|
@ -84,7 +89,7 @@ export const OptionsListPopoverInvalidSelections = () => {
|
||||||
size="s"
|
size="s"
|
||||||
/>
|
/>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
<EuiFlexItem>
|
<EuiFlexItem grow={false}>
|
||||||
<label>
|
<label>
|
||||||
{OptionsListStrings.popover.getInvalidSelectionsSectionTitle(invalidSelections.size)}
|
{OptionsListStrings.popover.getInvalidSelectionsSectionTitle(invalidSelections.size)}
|
||||||
</label>
|
</label>
|
||||||
|
@ -97,7 +102,7 @@ export const OptionsListPopoverInvalidSelections = () => {
|
||||||
invalidSelections.size
|
invalidSelections.size
|
||||||
)}
|
)}
|
||||||
options={selectableOptions}
|
options={selectableOptions}
|
||||||
listProps={{ onFocusBadge: false, isVirtualized: false }}
|
listProps={{ onFocusBadge: false }}
|
||||||
onChange={(newSuggestions, _, changedOption) => {
|
onChange={(newSuggestions, _, changedOption) => {
|
||||||
setSelectableOptions(newSuggestions);
|
setSelectableOptions(newSuggestions);
|
||||||
componentApi.deselectOption(changedOption.key);
|
componentApi.deselectOption(changedOption.key);
|
||||||
|
|
|
@ -20,3 +20,4 @@ export const OPTIONS_LIST_DEFAULT_SORT: OptionsListSortingType = {
|
||||||
|
|
||||||
export const MIN_OPTIONS_LIST_REQUEST_SIZE = 10;
|
export const MIN_OPTIONS_LIST_REQUEST_SIZE = 10;
|
||||||
export const MAX_OPTIONS_LIST_REQUEST_SIZE = 1000;
|
export const MAX_OPTIONS_LIST_REQUEST_SIZE = 1000;
|
||||||
|
export const MAX_OPTIONS_LIST_BULK_SELECT_SIZE = 100;
|
||||||
|
|
|
@ -398,6 +398,34 @@ export const getOptionsListControlFactory = (): DataControlFactory<
|
||||||
setSort: (sort: OptionsListSortingType | undefined) => {
|
setSort: (sort: OptionsListSortingType | undefined) => {
|
||||||
sort$.next(sort);
|
sort$.next(sort);
|
||||||
},
|
},
|
||||||
|
selectAll: (keys: string[]) => {
|
||||||
|
const field = api.field$.getValue();
|
||||||
|
if (keys.length < 1 || !field) {
|
||||||
|
api.setBlockingError(
|
||||||
|
new Error(OptionsListStrings.control.getInvalidSelectionMessage())
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedOptions = selectionsManager.api.selectedOptions$.getValue() ?? [];
|
||||||
|
const newSelections = keys.filter((key) => !selectedOptions.includes(key as string));
|
||||||
|
selectionsManager.api.setSelectedOptions([...selectedOptions, ...newSelections]);
|
||||||
|
},
|
||||||
|
deselectAll: (keys: string[]) => {
|
||||||
|
const field = api.field$.getValue();
|
||||||
|
if (keys.length < 1 || !field) {
|
||||||
|
api.setBlockingError(
|
||||||
|
new Error(OptionsListStrings.control.getInvalidSelectionMessage())
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedOptions = selectionsManager.api.selectedOptions$.getValue() ?? [];
|
||||||
|
const remainingSelections = selectedOptions.filter(
|
||||||
|
(option) => !keys.includes(option as string)
|
||||||
|
);
|
||||||
|
selectionsManager.api.setSelectedOptions(remainingSelections);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (selectionsManager.api.hasInitialSelections) {
|
if (selectionsManager.api.hasInitialSelections) {
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { OptionsListSearchTechnique } from '../../../../common/options_list/suggestions_searching';
|
import { OptionsListSearchTechnique } from '../../../../common/options_list/suggestions_searching';
|
||||||
|
import { MAX_OPTIONS_LIST_BULK_SELECT_SIZE } from './constants';
|
||||||
|
|
||||||
export const OptionsListStrings = {
|
export const OptionsListStrings = {
|
||||||
control: {
|
control: {
|
||||||
|
@ -128,6 +129,11 @@ export const OptionsListStrings = {
|
||||||
defaultMessage: 'Popover for {fieldName} control',
|
defaultMessage: 'Popover for {fieldName} control',
|
||||||
values: { fieldName },
|
values: { fieldName },
|
||||||
}),
|
}),
|
||||||
|
getSearchAriaLabel: (fieldName: string) =>
|
||||||
|
i18n.translate('controls.optionsList.popover.ariaLabel', {
|
||||||
|
defaultMessage: 'Filter suggestions for {fieldName} control',
|
||||||
|
values: { fieldName },
|
||||||
|
}),
|
||||||
getSuggestionsAriaLabel: (fieldName: string, optionCount: number) =>
|
getSuggestionsAriaLabel: (fieldName: string, optionCount: number) =>
|
||||||
i18n.translate('controls.optionsList.popover.suggestionsAriaLabel', {
|
i18n.translate('controls.optionsList.popover.suggestionsAriaLabel', {
|
||||||
defaultMessage:
|
defaultMessage:
|
||||||
|
@ -212,13 +218,13 @@ export const OptionsListStrings = {
|
||||||
getInvalidSelectionsSectionAriaLabel: (fieldName: string, invalidSelectionCount: number) =>
|
getInvalidSelectionsSectionAriaLabel: (fieldName: string, invalidSelectionCount: number) =>
|
||||||
i18n.translate('controls.optionsList.popover.invalidSelectionsAriaLabel', {
|
i18n.translate('controls.optionsList.popover.invalidSelectionsAriaLabel', {
|
||||||
defaultMessage:
|
defaultMessage:
|
||||||
'Invalid {invalidSelectionCount, plural, one {selection} other {selections}} for {fieldName}',
|
'Invalid {invalidSelectionCount, plural, one {selection} other {selections}} for {fieldName} ({invalidSelectionCount})',
|
||||||
values: { fieldName, invalidSelectionCount },
|
values: { fieldName, invalidSelectionCount },
|
||||||
}),
|
}),
|
||||||
getInvalidSelectionsSectionTitle: (invalidSelectionCount: number) =>
|
getInvalidSelectionsSectionTitle: (invalidSelectionCount: number) =>
|
||||||
i18n.translate('controls.optionsList.popover.invalidSelectionsSectionTitle', {
|
i18n.translate('controls.optionsList.popover.invalidSelectionsSectionTitle', {
|
||||||
defaultMessage:
|
defaultMessage:
|
||||||
'Invalid {invalidSelectionCount, plural, one {selection} other {selections}}',
|
'Invalid {invalidSelectionCount, plural, one {selection} other {selections}} ({invalidSelectionCount})',
|
||||||
values: { invalidSelectionCount },
|
values: { invalidSelectionCount },
|
||||||
}),
|
}),
|
||||||
getInvalidSelectionsLabel: (selectedOptions: number) =>
|
getInvalidSelectionsLabel: (selectedOptions: number) =>
|
||||||
|
@ -267,6 +273,15 @@ export const OptionsListStrings = {
|
||||||
'Appears in {documentCount, number} {documentCount, plural, one {document} other {documents}}',
|
'Appears in {documentCount, number} {documentCount, plural, one {document} other {documents}}',
|
||||||
values: { documentCount },
|
values: { documentCount },
|
||||||
}),
|
}),
|
||||||
|
getSelectAllButtonLabel: () =>
|
||||||
|
i18n.translate('controls.optionsList.popover.selectAllButtonLabel', {
|
||||||
|
defaultMessage: 'Select all',
|
||||||
|
}),
|
||||||
|
getMaximumBulkSelectionTooltip: () =>
|
||||||
|
i18n.translate('controls.optionsList.popover.maximumBulkSelectionTooltip', {
|
||||||
|
defaultMessage: 'Bulk selection is only available for fewer than {maxOptions} options',
|
||||||
|
values: { maxOptions: MAX_OPTIONS_LIST_BULK_SELECT_SIZE },
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
controlAndPopover: {
|
controlAndPopover: {
|
||||||
getExists: (negate: number = +false) =>
|
getExists: (negate: number = +false) =>
|
||||||
|
|
|
@ -52,4 +52,6 @@ export type OptionsListComponentApi = OptionsListControlApi &
|
||||||
loadMoreSubject: Subject<void>;
|
loadMoreSubject: Subject<void>;
|
||||||
sort$: PublishingSubject<OptionsListSortingType | undefined>;
|
sort$: PublishingSubject<OptionsListSortingType | undefined>;
|
||||||
setSort: (sort: OptionsListSortingType | undefined) => void;
|
setSort: (sort: OptionsListSortingType | undefined) => void;
|
||||||
|
selectAll: (keys: string[]) => void;
|
||||||
|
deselectAll: (keys: string[]) => void;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue