Skip to content

Commit 6ff9f18

Browse files
apinkertkarelhala
andauthored
feat(common-auth): add multi-select and active toggle to Users Table (#1764)
* feat: add activate toggle to users table * feat: remove duplicate calls from ActivateToggle and add bulk deactivate * feat: add activate users model and refactor checkedstates for toggles * feat: add bulk select/deselect for users table * feat: change handleToggle to use a users array * test: update users table props to fix tests * fix(use-flag): update src/smart-components/user/user-table-helpers.tsx * fix(use-flag): update src/smart-components/user/user-table-helpers.tsx * fix(use-flag): update src/smart-components/user/users-list-not-selectable.tsx * fix(lint): update src/smart-components/user/users-list-not-selectable.tsx * fix(test): update src/test/smart-components/user/users.test.js * fix(test): update src/test/smart-components/user/users.test.js --------- Co-authored-by: Karel Hala <[email protected]>
1 parent ae24726 commit 6ff9f18

File tree

8 files changed

+275
-52
lines changed

8 files changed

+275
-52
lines changed

src/Messages.js

+21
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,27 @@ export default defineMessages({
144144
description: 'activate users button text',
145145
defaultMessage: 'Activate users',
146146
},
147+
activateUsersConfirmationModalTitle: {
148+
id: 'activateUsersConfirmationModalTitle',
149+
description: 'activate users confirmation modal title text',
150+
defaultMessage: 'Activate users',
151+
},
152+
activateUsersConfirmationModalDescription: {
153+
id: 'activateUsersConfirmationModalDescription',
154+
description: 'activate users confirmation modal description text',
155+
defaultMessage: 'Are you sure you want to activate the user(s) below for your Red Hat organization?',
156+
},
157+
activateUsersConfirmationModalCheckboxText: {
158+
id: 'activateUsersConfirmationModalCheckboxText',
159+
description: 'activate users confirmation modal checkbox text',
160+
defaultMessage: 'Yes, I confirm that I want to add these users',
161+
},
162+
activateUsersConfirmationButton: {
163+
id: 'activateUsersConfirmationButton',
164+
description: 'activate users confirmation button text',
165+
defaultMessage: 'Activate user(s)',
166+
},
167+
147168
deactivateUsersButton: {
148169
id: 'deactivateUsersButton',
149170
description: 'deactivate users button text',

src/presentational-components/shared/table-composable-toolbar-view.tsx

+22-9
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { Fragment } from 'react';
1+
import React, { Fragment, ReactNode } from 'react';
22
import { useIntl } from 'react-intl';
33
import messages from '../../Messages';
44
import { TableVariant, Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table';
@@ -10,14 +10,18 @@ import Toolbar, { paginationBuilder } from './toolbar';
1010
import EmptyWithAction from './empty-state';
1111
import './table-toolbar-view.scss';
1212
import { ISortBy, OnSort } from '@patternfly/react-table';
13-
import { CellObject } from '../../smart-components/user/user-table-helpers';
13+
import { CellObject, CellType, SelectCell } from '../../smart-components/user/user-table-helpers';
1414

1515
interface FilterProps {
1616
username?: string;
1717
email?: string;
1818
status?: string[];
1919
}
2020

21+
function isSelectCell(cell: any): cell is SelectCell {
22+
return typeof cell === 'object' && typeof cell.select !== 'undefined';
23+
}
24+
2125
interface FetchDataProps {
2226
limit?: number;
2327
offset?: number;
@@ -54,7 +58,9 @@ interface MainTableProps {
5458
setFilterValue: (value: FilterProps) => void;
5559
pagination: { limit?: number; offset?: number; count?: number; noBottom?: boolean };
5660
fetchData: (config: FetchDataProps) => void;
57-
toolbarButtons?: () => (React.JSX.Element | React.ReactNode)[];
61+
toolbarButtons?: () => (React.JSX.Element | { label: string; props: unknown; onClick: () => void })[];
62+
setCheckedItems?: (callback: (selected: unknown[]) => void) => void;
63+
checkedRows?: unknown[];
5864
filterPlaceholder?: string;
5965
filters: Array<{
6066
value: string | number | Array<unknown>;
@@ -90,13 +96,14 @@ const MainTable = ({
9096
isSelectable,
9197
isLoading,
9298
noData,
93-
data,
9499
title,
95100
filterValue,
96101
setFilterValue,
97102
pagination,
98103
fetchData,
99104
toolbarButtons,
105+
setCheckedItems,
106+
checkedRows,
100107
filterPlaceholder,
101108
filters,
102109
isFilterable,
@@ -125,10 +132,12 @@ const MainTable = ({
125132
<Toolbar
126133
isSelectable={isSelectable}
127134
isLoading={isLoading || noData}
128-
data={data}
135+
data={rows}
129136
titleSingular={title.singular}
130137
filterValue={filterValue}
131138
setFilterValue={setFilterValue}
139+
setCheckedItems={setCheckedItems}
140+
checkedRows={checkedRows}
132141
sortBy={orderBy}
133142
pagination={pagination}
134143
fetchData={fetchData}
@@ -167,10 +176,10 @@ const MainTable = ({
167176
{rows?.length > 0 ? (
168177
rows?.map((row, i) => (
169178
<Tr key={i}>
170-
{row.cells.map((cell: CellObject, j: number) => (
171-
<Td key={j} dataLabel={columns[j].title}>
179+
{row.cells.map((cell: CellType, j: number) => (
180+
<Td key={j} dataLabel={columns[j].title} {...(isSelectCell(cell) && cell)}>
172181
{/* TODO: make more general */}
173-
{isCellObject(cell) ? (cell.title as string) : (cell as unknown as React.ReactNode)}
182+
{isCellObject(cell) ? (cell.title as string) : isSelectCell(cell) ? null : (cell as unknown as React.ReactNode)}
174183
</Td>
175184
))}
176185
</Tr>
@@ -240,6 +249,8 @@ export const TableComposableToolbarView = ({
240249
isLoading,
241250
emptyFilters,
242251
setFilterValue,
252+
setCheckedItems,
253+
checkedRows,
243254
isSelectable = false,
244255
fetchData,
245256
emptyProps,
@@ -272,11 +283,12 @@ export const TableComposableToolbarView = ({
272283
intl.formatMessage(messages.toConfigureUserAccess),
273284
intl.formatMessage(messages.createAtLeastOneItem, { item: title.singular }),
274285
]}
275-
actions={toolbarButtons ? toolbarButtons()[0] : false}
286+
actions={toolbarButtons ? (toolbarButtons()[0] as ReactNode) : false}
276287
{...(typeof emptyProps === 'object' ? emptyProps : {})}
277288
/>
278289
) : (
279290
<MainTable
291+
setCheckedItems={setCheckedItems}
280292
columns={columns}
281293
isSelectable={isSelectable}
282294
isLoading={isLoading}
@@ -307,6 +319,7 @@ export const TableComposableToolbarView = ({
307319
ouiaId={ouiaId}
308320
noDataDescription={noDataDescription}
309321
emptyFilters={emptyFilters}
322+
checkedRows={checkedRows}
310323
/>
311324
)}
312325
</Fragment>

src/smart-components/group/add-group/users-list.js

+1
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ const UsersList = ({ selectedUsers, setSelectedUsers, userLinks, usesMetaInURL,
104104
usesMetaInURL && updateStateFilters(payload);
105105
setFilters({ username: '', ...payload });
106106
};
107+
107108
return (
108109
<TableToolbarView
109110
isCompact
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import React from 'react';
2+
import { IntlShape } from 'react-intl';
3+
import messages from '../../Messages';
4+
import { Switch } from '@patternfly/react-core';
5+
import { UserProps } from './user-table-helpers';
6+
7+
const ActivateToggle: React.FC<{
8+
user: UserProps;
9+
handleToggle: (_ev: unknown, isActive: boolean, updatedUsers: any[]) => void;
10+
intl: IntlShape;
11+
}> = ({ user, handleToggle, intl }) => {
12+
return user.external_source_id ? (
13+
<Switch
14+
id={user.username}
15+
key={user.uuid}
16+
isChecked={user.is_active}
17+
onChange={(e, value) => handleToggle(e, value, [user])}
18+
label={intl.formatMessage(messages['usersAndUserGroupsActive'])}
19+
labelOff={intl.formatMessage(messages['usersAndUserGroupsInactive'])}
20+
></Switch>
21+
) : (
22+
<></>
23+
);
24+
};
25+
26+
export default ActivateToggle;

src/smart-components/user/user-table-helpers.tsx

+58-23
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import React, { Fragment } from 'react';
2-
import { Label } from '@patternfly/react-core';
32
import { IntlShape } from 'react-intl';
43
import messages from '../../Messages';
54
import pathnames from '../../utilities/pathnames';
65
import AppLink from '../../presentational-components/shared/AppLink';
76
import OrgAdminDropdown from './OrgAdminDropdown';
87
import { CheckIcon, CloseIcon } from '@patternfly/react-icons';
9-
8+
import ActivateToggle from './ActivateToggle';
9+
import { Label } from '@patternfly/react-core';
1010
export interface UserProps {
11+
isSelected: boolean;
1112
email: string;
1213
first_name: string;
1314
is_active: boolean;
@@ -19,17 +20,18 @@ export interface UserProps {
1920
}
2021

2122
export type CellObject = { title: string | React.RefAttributes<HTMLAnchorElement>; props?: { 'data-is-active': boolean } };
23+
export type SelectCell = {
24+
select: {
25+
rowIndex: number;
26+
onSelect: (_event: unknown, isSelecting: boolean) => void;
27+
isSelected: boolean;
28+
};
29+
};
30+
export type CellType = SelectCell | React.ReactNode | CellObject | string;
2231

2332
export interface RowProps {
2433
uuid: string; // username
25-
cells: [
26-
React.ReactNode, // yes or no for isOrgAdmin
27-
CellObject, // link to user or just username
28-
string, // email
29-
string, // firstName
30-
string, // lastName
31-
CellObject // status
32-
];
34+
cells: CellType[];
3335
selected: boolean;
3436
authModel?: boolean;
3537
orgAdmin?: boolean;
@@ -42,15 +44,43 @@ export const createRows = (
4244
intl: IntlShape,
4345
checkedRows = [],
4446
isSelectable = false,
47+
onSelectUser?: (user: UserProps, isSelecting: boolean) => void,
48+
handleToggle?: (_ev: unknown, isActive: boolean, updatedUsers: any[]) => void,
4549
authModel?: boolean,
4650
orgAdmin?: boolean,
4751
fetchData?: () => void
48-
): RowProps[] =>
49-
data?.reduce<RowProps[]>(
50-
(acc, { username, is_active: isActive, email, first_name: firstName, last_name: lastName, is_org_admin: isOrgAdmin, external_source_id }) => {
52+
): RowProps[] => {
53+
return data?.reduce<RowProps[]>(
54+
(
55+
acc,
56+
{ isSelected, username, is_active: isActive, email, first_name: firstName, last_name: lastName, is_org_admin: isOrgAdmin, external_source_id },
57+
rowIndex
58+
) => {
59+
const user = {
60+
isSelected,
61+
username,
62+
is_active: isActive,
63+
email,
64+
first_name: firstName,
65+
last_name: lastName,
66+
is_org_admin: isOrgAdmin,
67+
external_source_id,
68+
uuid: username,
69+
};
5170
const newEntry: RowProps = {
5271
uuid: username,
5372
cells: [
73+
...(onSelectUser && isOrgAdmin && authModel
74+
? [
75+
{
76+
select: {
77+
rowIndex,
78+
onSelect: (_event: unknown, isSelecting: boolean) => onSelectUser(user, isSelecting),
79+
isSelected: isSelected,
80+
},
81+
},
82+
]
83+
: []),
5484
authModel && orgAdmin ? (
5585
<OrgAdminDropdown
5686
key={`dropdown-${username}`}
@@ -81,16 +111,20 @@ export const createRows = (
81111
email,
82112
firstName,
83113
lastName,
84-
{
85-
title: (
86-
<Label key="status" color={isActive ? 'green' : 'grey'}>
87-
{intl.formatMessage(isActive ? messages.active : messages.inactive)}
88-
</Label>
89-
),
90-
props: {
91-
'data-is-active': isActive,
92-
},
93-
},
114+
...(handleToggle && isOrgAdmin && authModel
115+
? [<ActivateToggle key="active-toggle" user={user} handleToggle={handleToggle} intl={intl} />]
116+
: [
117+
{
118+
title: (
119+
<Label key="status" color={isActive ? 'green' : 'grey'}>
120+
{intl.formatMessage(isActive ? messages.active : messages.inactive)}
121+
</Label>
122+
),
123+
props: {
124+
'data-is-active': isActive,
125+
},
126+
},
127+
]),
94128
],
95129
selected: isSelectable ? Boolean(checkedRows?.find?.(({ uuid }) => uuid === username)) : false,
96130
};
@@ -99,3 +133,4 @@ export const createRows = (
99133
},
100134
[]
101135
);
136+
};

0 commit comments

Comments
 (0)