mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-16 05:04:57 +08:00
Device manager - select all devices (#9330)
* add device selection that does nothing * multi select and sign out of sessions * test multiple selection * fix type after rebase * select all sessions
This commit is contained in:
parent
0ded5e0505
commit
c59bbdf917
@ -25,7 +25,7 @@ limitations under the License.
|
||||
display: grid;
|
||||
grid-gap: $spacing-16;
|
||||
margin: 0;
|
||||
padding: 0 $spacing-8;
|
||||
padding: 0 $spacing-16;
|
||||
}
|
||||
|
||||
.mx_FilteredDeviceList_listItem {
|
||||
|
@ -266,8 +266,21 @@ export const FilteredDeviceList =
|
||||
onFilterChange(filterId === ALL_FILTER_ID ? undefined : filterId as DeviceSecurityVariation);
|
||||
};
|
||||
|
||||
const isAllSelected = selectedDeviceIds.length >= sortedDevices.length;
|
||||
const toggleSelectAll = () => {
|
||||
if (isAllSelected) {
|
||||
setSelectedDeviceIds([]);
|
||||
} else {
|
||||
setSelectedDeviceIds(sortedDevices.map(device => device.device_id));
|
||||
}
|
||||
};
|
||||
|
||||
return <div className='mx_FilteredDeviceList' ref={ref}>
|
||||
<FilteredDeviceListHeader selectedDeviceCount={selectedDeviceIds.length}>
|
||||
<FilteredDeviceListHeader
|
||||
selectedDeviceCount={selectedDeviceIds.length}
|
||||
isAllSelected={isAllSelected}
|
||||
toggleSelectAll={toggleSelectAll}
|
||||
>
|
||||
{ selectedDeviceIds.length
|
||||
? <>
|
||||
<AccessibleButton
|
||||
|
@ -17,18 +17,39 @@ limitations under the License.
|
||||
import React, { HTMLProps } from 'react';
|
||||
|
||||
import { _t } from '../../../../languageHandler';
|
||||
import StyledCheckbox, { CheckboxStyle } from '../../elements/StyledCheckbox';
|
||||
import { Alignment } from '../../elements/Tooltip';
|
||||
import TooltipTarget from '../../elements/TooltipTarget';
|
||||
|
||||
interface Props extends Omit<HTMLProps<HTMLDivElement>, 'className'> {
|
||||
selectedDeviceCount: number;
|
||||
isAllSelected: boolean;
|
||||
toggleSelectAll: () => void;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const FilteredDeviceListHeader: React.FC<Props> = ({
|
||||
selectedDeviceCount,
|
||||
isAllSelected,
|
||||
toggleSelectAll,
|
||||
children,
|
||||
...rest
|
||||
}) => {
|
||||
const checkboxLabel = isAllSelected ? _t('Deselect all') : _t('Select all');
|
||||
return <div className='mx_FilteredDeviceListHeader' {...rest}>
|
||||
<TooltipTarget
|
||||
label={checkboxLabel}
|
||||
alignment={Alignment.Top}
|
||||
>
|
||||
<StyledCheckbox
|
||||
kind={CheckboxStyle.Solid}
|
||||
checked={isAllSelected}
|
||||
onChange={toggleSelectAll}
|
||||
id='device-select-all-checkbox'
|
||||
data-testid='device-select-all-checkbox'
|
||||
aria-label={checkboxLabel}
|
||||
/>
|
||||
</TooltipTarget>
|
||||
<span className='mx_FilteredDeviceListHeader_label'>
|
||||
{ selectedDeviceCount > 0
|
||||
? _t('%(selectedDeviceCount)s sessions selected', { selectedDeviceCount })
|
||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import FilteredDeviceListHeader from '../../../../../src/components/views/settings/devices/FilteredDeviceListHeader';
|
||||
@ -22,6 +22,8 @@ import FilteredDeviceListHeader from '../../../../../src/components/views/settin
|
||||
describe('<FilteredDeviceListHeader />', () => {
|
||||
const defaultProps = {
|
||||
selectedDeviceCount: 0,
|
||||
isAllSelected: false,
|
||||
toggleSelectAll: jest.fn(),
|
||||
children: <div>test</div>,
|
||||
['data-testid']: 'test123',
|
||||
};
|
||||
@ -32,8 +34,21 @@ describe('<FilteredDeviceListHeader />', () => {
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly when all devices are selected', () => {
|
||||
const { container } = render(getComponent({ isAllSelected: true }));
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly when some devices are selected', () => {
|
||||
const { getByText } = render(getComponent({ selectedDeviceCount: 2 }));
|
||||
expect(getByText('2 sessions selected')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('clicking checkbox toggles selection', () => {
|
||||
const toggleSelectAll = jest.fn();
|
||||
const { getByTestId } = render(getComponent({ toggleSelectAll }));
|
||||
fireEvent.click(getByTestId('device-select-all-checkbox'));
|
||||
|
||||
expect(toggleSelectAll).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
@ -1,11 +1,80 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<FilteredDeviceListHeader /> renders correctly when no devices are selected 1`] = `
|
||||
exports[`<FilteredDeviceListHeader /> renders correctly when all devices are selected 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_FilteredDeviceListHeader"
|
||||
data-testid="test123"
|
||||
>
|
||||
<div
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
|
||||
>
|
||||
<input
|
||||
aria-label="Deselect all"
|
||||
checked=""
|
||||
data-testid="device-select-all-checkbox"
|
||||
id="device-select-all-checkbox"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label
|
||||
for="device-select-all-checkbox"
|
||||
>
|
||||
<div
|
||||
class="mx_Checkbox_background"
|
||||
>
|
||||
<div
|
||||
class="mx_Checkbox_checkmark"
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="mx_FilteredDeviceListHeader_label"
|
||||
>
|
||||
Sessions
|
||||
</span>
|
||||
<div>
|
||||
test
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<FilteredDeviceListHeader /> renders correctly when no devices are selected 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_FilteredDeviceListHeader"
|
||||
data-testid="test123"
|
||||
>
|
||||
<div
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
|
||||
>
|
||||
<input
|
||||
aria-label="Select all"
|
||||
data-testid="device-select-all-checkbox"
|
||||
id="device-select-all-checkbox"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label
|
||||
for="device-select-all-checkbox"
|
||||
>
|
||||
<div
|
||||
class="mx_Checkbox_background"
|
||||
>
|
||||
<div
|
||||
class="mx_Checkbox_checkmark"
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="mx_FilteredDeviceListHeader_label"
|
||||
>
|
||||
|
@ -38,10 +38,17 @@ import {
|
||||
getMockClientWithEventEmitter,
|
||||
mkPusher,
|
||||
mockClientMethodsUser,
|
||||
mockPlatformPeg,
|
||||
} from '../../../../../test-utils';
|
||||
import Modal from '../../../../../../src/Modal';
|
||||
import LogoutDialog from '../../../../../../src/components/views/dialogs/LogoutDialog';
|
||||
import { DeviceWithVerification } from '../../../../../../src/components/views/settings/devices/types';
|
||||
import {
|
||||
DeviceSecurityVariation,
|
||||
DeviceWithVerification,
|
||||
} from '../../../../../../src/components/views/settings/devices/types';
|
||||
import { INACTIVE_DEVICE_AGE_MS } from '../../../../../../src/components/views/settings/devices/filter';
|
||||
|
||||
mockPlatformPeg();
|
||||
|
||||
describe('<SessionManagerTab />', () => {
|
||||
const aliceId = '@alice:server.org';
|
||||
@ -61,6 +68,11 @@ describe('<SessionManagerTab />', () => {
|
||||
last_seen_ts: Date.now() - 600000,
|
||||
};
|
||||
|
||||
const alicesInactiveDevice = {
|
||||
device_id: 'alices_older_mobile_device',
|
||||
last_seen_ts: Date.now() - (INACTIVE_DEVICE_AGE_MS + 1000),
|
||||
};
|
||||
|
||||
const mockCrossSigningInfo = {
|
||||
checkDeviceTrust: jest.fn(),
|
||||
};
|
||||
@ -108,11 +120,28 @@ describe('<SessionManagerTab />', () => {
|
||||
fireEvent.click(checkbox);
|
||||
};
|
||||
|
||||
const setFilter = async (
|
||||
container: HTMLElement,
|
||||
option: DeviceSecurityVariation | string,
|
||||
) => await act(async () => {
|
||||
const dropdown = container.querySelector('[aria-label="Filter devices"]');
|
||||
|
||||
fireEvent.click(dropdown as Element);
|
||||
// tick to let dropdown render
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
fireEvent.click(container.querySelector(`#device-list-filter__${option}`) as Element);
|
||||
});
|
||||
|
||||
const isDeviceSelected = (
|
||||
getByTestId: ReturnType<typeof render>['getByTestId'],
|
||||
deviceId: DeviceWithVerification['device_id'],
|
||||
): boolean => !!(getByTestId(`device-tile-checkbox-${deviceId}`) as HTMLInputElement).checked;
|
||||
|
||||
const isSelectAllChecked = (
|
||||
getByTestId: ReturnType<typeof render>['getByTestId'],
|
||||
): boolean => !!(getByTestId('device-select-all-checkbox') as HTMLInputElement).checked;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.spyOn(logger, 'error').mockRestore();
|
||||
@ -811,6 +840,96 @@ describe('<SessionManagerTab />', () => {
|
||||
// unselected
|
||||
expect(isDeviceSelected(getByTestId, alicesOlderMobileDevice.device_id)).toBeFalsy();
|
||||
});
|
||||
|
||||
describe('toggling select all', () => {
|
||||
it('selects all sessions when there is not existing selection', async () => {
|
||||
const { getByTestId, getByText } = render(getComponent());
|
||||
|
||||
await act(async () => {
|
||||
await flushPromisesWithFakeTimers();
|
||||
});
|
||||
|
||||
fireEvent.click(getByTestId('device-select-all-checkbox'));
|
||||
|
||||
// header displayed correctly
|
||||
expect(getByText('2 sessions selected')).toBeTruthy();
|
||||
expect(isSelectAllChecked(getByTestId)).toBeTruthy();
|
||||
|
||||
// devices selected
|
||||
expect(isDeviceSelected(getByTestId, alicesMobileDevice.device_id)).toBeTruthy();
|
||||
expect(isDeviceSelected(getByTestId, alicesOlderMobileDevice.device_id)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('selects all sessions when some sessions are already selected', async () => {
|
||||
const { getByTestId, getByText } = render(getComponent());
|
||||
|
||||
await act(async () => {
|
||||
await flushPromisesWithFakeTimers();
|
||||
});
|
||||
|
||||
toggleDeviceSelection(getByTestId, alicesMobileDevice.device_id);
|
||||
|
||||
fireEvent.click(getByTestId('device-select-all-checkbox'));
|
||||
|
||||
// header displayed correctly
|
||||
expect(getByText('2 sessions selected')).toBeTruthy();
|
||||
expect(isSelectAllChecked(getByTestId)).toBeTruthy();
|
||||
|
||||
// devices selected
|
||||
expect(isDeviceSelected(getByTestId, alicesMobileDevice.device_id)).toBeTruthy();
|
||||
expect(isDeviceSelected(getByTestId, alicesOlderMobileDevice.device_id)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('deselects all sessions when all sessions are selected', async () => {
|
||||
const { getByTestId, getByText } = render(getComponent());
|
||||
|
||||
await act(async () => {
|
||||
await flushPromisesWithFakeTimers();
|
||||
});
|
||||
|
||||
fireEvent.click(getByTestId('device-select-all-checkbox'));
|
||||
|
||||
// header displayed correctly
|
||||
expect(getByText('2 sessions selected')).toBeTruthy();
|
||||
expect(isSelectAllChecked(getByTestId)).toBeTruthy();
|
||||
|
||||
// devices selected
|
||||
expect(isDeviceSelected(getByTestId, alicesMobileDevice.device_id)).toBeTruthy();
|
||||
expect(isDeviceSelected(getByTestId, alicesOlderMobileDevice.device_id)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('selects only sessions that are part of the active filter', async () => {
|
||||
mockClient.getDevices.mockResolvedValue({ devices: [
|
||||
alicesDevice,
|
||||
alicesMobileDevice,
|
||||
alicesInactiveDevice,
|
||||
] });
|
||||
const { getByTestId, container } = render(getComponent());
|
||||
|
||||
await act(async () => {
|
||||
await flushPromisesWithFakeTimers();
|
||||
});
|
||||
|
||||
// filter for inactive sessions
|
||||
await setFilter(container, DeviceSecurityVariation.Inactive);
|
||||
|
||||
// select all inactive sessions
|
||||
fireEvent.click(getByTestId('device-select-all-checkbox'));
|
||||
|
||||
expect(isSelectAllChecked(getByTestId)).toBeTruthy();
|
||||
|
||||
// sign out of all selected sessions
|
||||
fireEvent.click(getByTestId('sign-out-selection-cta'));
|
||||
|
||||
// only called with session from active filter
|
||||
expect(mockClient.deleteMultipleDevices).toHaveBeenCalledWith(
|
||||
[
|
||||
alicesInactiveDevice.device_id,
|
||||
],
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("lets you change the pusher state", async () => {
|
||||
|
@ -19,6 +19,31 @@ exports[`<SessionManagerTab /> goes to filtered list from security recommendatio
|
||||
<div
|
||||
class="mx_FilteredDeviceListHeader"
|
||||
>
|
||||
<div
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
|
||||
>
|
||||
<input
|
||||
aria-label="Select all"
|
||||
data-testid="device-select-all-checkbox"
|
||||
id="device-select-all-checkbox"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label
|
||||
for="device-select-all-checkbox"
|
||||
>
|
||||
<div
|
||||
class="mx_Checkbox_background"
|
||||
>
|
||||
<div
|
||||
class="mx_Checkbox_checkmark"
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="mx_FilteredDeviceListHeader_label"
|
||||
>
|
||||
|
Loading…
Reference in New Issue
Block a user