Skip to content

Commit 21bf567

Browse files
authored
Add full name field and formatting for user identities (#153)
Signed-off-by: achour94 <[email protected]>
1 parent 9814a97 commit 21bf567

File tree

10 files changed

+200
-71
lines changed

10 files changed

+200
-71
lines changed

src/pages/common/table-selection.tsx

Lines changed: 75 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,96 +5,120 @@
55
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
66
*/
77

8-
import { FunctionComponent, useCallback, useMemo, useRef, useState } from 'react';
8+
import { useCallback, useMemo, useRef, useState } from 'react';
99
import { FormattedMessage } from 'react-intl';
1010
import { CustomAGGrid } from '@gridsuite/commons-ui';
1111
import { Grid, Typography } from '@mui/material';
1212
import { AgGridReact } from 'ag-grid-react';
1313
import { ColDef, GetRowIdParams, GridReadyEvent } from 'ag-grid-community';
1414
import { defaultColDef, defaultRowSelection } from './table-config';
1515

16-
export interface TableSelectionProps {
17-
itemName: string;
18-
tableItems: string[];
19-
tableSelectedItems?: string[];
20-
onSelectionChanged: (selectedItems: string[]) => void;
16+
/**
17+
* Generic props for TableSelection component.
18+
* @template TData - The type of data items in the table
19+
*/
20+
export interface TableSelectionProps<TData> {
21+
/** Translation key for the table title */
22+
titleId: string;
23+
/** Array of data items to display */
24+
items: TData[];
25+
/** Function to extract the unique ID from each item (used for selection tracking) */
26+
getItemId: (item: TData) => string;
27+
/** Column definitions for the AG Grid */
28+
columnDefs: ColDef<TData>[];
29+
/** Array of selected item IDs */
30+
selectedIds?: string[];
31+
/** Callback when selection changes, receives array of selected IDs */
32+
onSelectionChanged: (selectedIds: string[]) => void;
2133
}
2234

2335
const rowSelection = {
2436
...defaultRowSelection,
2537
headerCheckbox: false,
2638
};
2739

28-
const TableSelection: FunctionComponent<TableSelectionProps> = (props) => {
29-
const [selectedRowsLength, setSelectedRowsLength] = useState(0);
30-
const gridRef = useRef<AgGridReact>(null);
40+
/**
41+
* A generic table component with row selection support.
42+
* Displays data in an AG Grid with checkboxes for selection.
43+
*/
44+
function TableSelection<TData>({
45+
titleId,
46+
items,
47+
getItemId,
48+
columnDefs,
49+
selectedIds,
50+
onSelectionChanged,
51+
}: Readonly<TableSelectionProps<TData>>) {
52+
const [selectedCount, setSelectedCount] = useState(0);
53+
const gridRef = useRef<AgGridReact<TData>>(null);
54+
55+
const getRowId = useCallback(
56+
(params: GetRowIdParams<TData>): string => {
57+
return getItemId(params.data);
58+
},
59+
[getItemId]
60+
);
3161

32-
const handleEquipmentSelectionChanged = useCallback(() => {
62+
const handleSelectionChanged = useCallback(() => {
3363
const selectedRows = gridRef.current?.api.getSelectedRows();
3464
if (selectedRows == null) {
35-
setSelectedRowsLength(0);
36-
props.onSelectionChanged([]);
65+
setSelectedCount(0);
66+
onSelectionChanged([]);
3767
} else {
38-
setSelectedRowsLength(selectedRows.length);
39-
props.onSelectionChanged(selectedRows.map((r) => r.id));
68+
setSelectedCount(selectedRows.length);
69+
onSelectionChanged(selectedRows.map(getItemId));
4070
}
41-
}, [props]);
42-
43-
const rowData = useMemo(() => {
44-
return props.tableItems.map((str) => ({ id: str }));
45-
}, [props.tableItems]);
71+
}, [onSelectionChanged, getItemId]);
4672

47-
const columnDefs = useMemo(
48-
(): ColDef[] => [
49-
{
50-
field: 'id',
51-
filter: true,
52-
initialSort: 'asc',
53-
tooltipField: 'id',
54-
flex: 1,
55-
},
56-
],
57-
[]
58-
);
59-
60-
function getRowId(params: GetRowIdParams): string {
61-
return params.data.id;
62-
}
63-
64-
const onGridReady = useCallback(
65-
({ api }: GridReadyEvent) => {
66-
api?.forEachNode((n) => {
67-
if (props.tableSelectedItems !== undefined && n.id && props.tableSelectedItems.includes(n.id)) {
68-
n.setSelected(true);
73+
const handleGridReady = useCallback(
74+
({ api }: GridReadyEvent<TData>) => {
75+
if (!selectedIds?.length) {
76+
return;
77+
}
78+
api.forEachNode((node) => {
79+
const nodeId = node.id;
80+
if (nodeId && selectedIds.includes(nodeId)) {
81+
node.setSelected(true);
6982
}
7083
});
7184
},
72-
[props.tableSelectedItems]
85+
[selectedIds]
7386
);
7487

88+
const mergedColumnDefs = useMemo((): ColDef<TData>[] => {
89+
return columnDefs.map((col, index) => ({
90+
filter: true,
91+
flex: 1,
92+
...col,
93+
// First column gets initial sort if not specified elsewhere
94+
...(index === 0 && !columnDefs.some((c) => c.initialSort) ? { initialSort: 'asc' as const } : {}),
95+
}));
96+
}, [columnDefs]);
97+
7598
return (
76-
<Grid item container direction={'column'} style={{ height: '100%' }}>
99+
<Grid item container direction="column" style={{ height: '100%' }}>
77100
<Grid item>
78101
<Typography variant="subtitle1">
79-
<FormattedMessage id={props.itemName}></FormattedMessage>
80-
{` (${selectedRowsLength} / ${rowData?.length ?? 0})`}
102+
<FormattedMessage id={titleId} />
103+
{` (${selectedCount} / ${items.length})`}
81104
</Typography>
82105
</Grid>
83106
<Grid item xs>
84107
<CustomAGGrid
85108
gridId="table-selection"
86109
ref={gridRef}
87-
rowData={rowData}
88-
columnDefs={columnDefs}
110+
rowData={items}
111+
columnDefs={mergedColumnDefs}
89112
defaultColDef={defaultColDef}
90113
rowSelection={rowSelection}
91114
getRowId={getRowId}
92-
onSelectionChanged={handleEquipmentSelectionChanged}
93-
onGridReady={onGridReady}
94-
accentedSort={true}
115+
onSelectionChanged={handleSelectionChanged}
116+
onGridReady={handleGridReady}
117+
accentedSort
95118
/>
96119
</Grid>
97120
</Grid>
98121
);
99-
};
122+
}
123+
100124
export default TableSelection;

src/pages/groups/modification/group-modification-dialog.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ import GroupModificationForm, {
1010
GroupModificationFormType,
1111
GroupModificationSchema,
1212
SELECTED_USERS,
13+
UserSelectionItem,
1314
} from './group-modification-form';
1415
import { yupResolver } from '@hookform/resolvers/yup';
1516
import { useForm } from 'react-hook-form';
1617
import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react';
1718
import { CustomMuiDialog, FetchStatus, useSnackMessage } from '@gridsuite/commons-ui';
18-
import { GroupInfos, UserAdminSrv, UserInfos } from '../../../services';
19+
import { formatFullName, GroupInfos, UserAdminSrv, UserInfos } from '../../../services';
1920

2021
interface GroupModificationDialogProps {
2122
groupInfos: GroupInfos | undefined;
@@ -35,7 +36,7 @@ const GroupModificationDialog: FunctionComponent<GroupModificationDialogProps> =
3536
resolver: yupResolver(GroupModificationSchema),
3637
});
3738
const { reset, setValue } = formMethods;
38-
const [userOptions, setUserOptions] = useState<string[]>([]);
39+
const [userOptions, setUserOptions] = useState<UserSelectionItem[]>([]);
3940
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
4041
const [dataFetchStatus, setDataFetchStatus] = useState<string>(FetchStatus.IDLE);
4142

@@ -54,7 +55,14 @@ const GroupModificationDialog: FunctionComponent<GroupModificationDialogProps> =
5455
.then((allUsers: UserInfos[]) => {
5556
setDataFetchStatus(FetchStatus.FETCH_SUCCESS);
5657
setUserOptions(
57-
allUsers?.map((p) => p.sub).sort((a: string, b: string) => a.localeCompare(b)) || []
58+
allUsers
59+
?.map(
60+
(p): UserSelectionItem => ({
61+
sub: p.sub,
62+
fullName: formatFullName(p.firstName, p.lastName),
63+
})
64+
)
65+
.sort((a, b) => a.sub.localeCompare(b.sub)) || []
5866
);
5967
})
6068
.catch((error) => {

src/pages/groups/modification/group-modification-form.tsx

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
66
*/
77

8-
import { type FunctionComponent } from 'react';
8+
import { useMemo, type FunctionComponent } from 'react';
99
import { Grid } from '@mui/material';
1010
import { TextInput, yupConfig as yup } from '@gridsuite/commons-ui';
1111
import TableSelection from '../../common/table-selection';
12+
import { useIntl } from 'react-intl';
13+
import { ColDef } from 'ag-grid-community';
1214

1315
export const GROUP_NAME = 'name';
1416
export const SELECTED_USERS = 'users';
@@ -22,9 +24,13 @@ export const GroupModificationSchema = yup
2224
.required();
2325

2426
export type GroupModificationFormType = yup.InferType<typeof GroupModificationSchema>;
27+
export interface UserSelectionItem {
28+
sub: string;
29+
fullName: string;
30+
}
2531

2632
interface GroupModificationFormProps {
27-
usersOptions: string[];
33+
usersOptions: UserSelectionItem[];
2834
selectedUsers?: string[];
2935
onSelectionChanged: (selectedItems: string[]) => void;
3036
}
@@ -34,16 +40,36 @@ const GroupModificationForm: FunctionComponent<GroupModificationFormProps> = ({
3440
selectedUsers,
3541
onSelectionChanged,
3642
}) => {
43+
const intl = useIntl();
44+
45+
const userColumnDefs = useMemo(
46+
(): ColDef<UserSelectionItem>[] => [
47+
{
48+
field: 'sub',
49+
headerName: intl.formatMessage({ id: 'users.table.id' }),
50+
tooltipField: 'sub',
51+
},
52+
{
53+
field: 'fullName',
54+
headerName: intl.formatMessage({ id: 'users.table.fullName' }),
55+
tooltipField: 'fullName',
56+
},
57+
],
58+
[intl]
59+
);
60+
3761
return (
3862
<Grid item container spacing={2} marginTop={0} style={{ height: '100%' }}>
3963
<Grid item xs={12}>
4064
<TextInput name={GROUP_NAME} label={'groups.table.id'} clearable={true} />
4165
</Grid>
4266
<Grid item xs={12} style={{ height: '90%' }}>
43-
<TableSelection
44-
itemName={'groups.table.users'}
45-
tableItems={usersOptions}
46-
tableSelectedItems={selectedUsers}
67+
<TableSelection<UserSelectionItem>
68+
titleId="groups.table.users"
69+
items={usersOptions}
70+
getItemId={(user) => user.sub}
71+
columnDefs={userColumnDefs}
72+
selectedIds={selectedUsers}
4773
onSelectionChanged={onSelectionChanged}
4874
/>
4975
</Grid>

src/pages/users/modification/user-modification-dialog.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
*/
77

88
import UserModificationForm, {
9+
GroupSelectionItem,
10+
USER_FULL_NAME,
911
USER_NAME,
1012
USER_PROFILE_NAME,
1113
USER_SELECTED_GROUPS,
@@ -16,7 +18,7 @@ import { yupResolver } from '@hookform/resolvers/yup';
1618
import { useForm } from 'react-hook-form';
1719
import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react';
1820
import { CustomMuiDialog, FetchStatus, useSnackMessage } from '@gridsuite/commons-ui';
19-
import { GroupInfos, UserAdminSrv, UserInfos, UserProfile } from '../../../services';
21+
import { formatFullName, GroupInfos, UserAdminSrv, UserInfos, UserProfile } from '../../../services';
2022

2123
interface UserModificationDialogProps {
2224
userInfos: UserInfos | undefined;
@@ -37,15 +39,17 @@ const UserModificationDialog: FunctionComponent<UserModificationDialogProps> = (
3739
});
3840
const { reset, setValue } = formMethods;
3941
const [profileOptions, setProfileOptions] = useState<string[]>([]);
40-
const [groupOptions, setGroupOptions] = useState<string[]>([]);
42+
const [groupOptions, setGroupOptions] = useState<GroupSelectionItem[]>([]);
4143
const [selectedGroups, setSelectedGroups] = useState<string[]>([]);
4244
const [dataFetchStatus, setDataFetchStatus] = useState<string>(FetchStatus.IDLE);
4345

4446
useEffect(() => {
4547
if (userInfos && open) {
4648
const sortedGroups = Array.from(userInfos.groups ?? []).sort((a, b) => a.localeCompare(b));
49+
const fullName = formatFullName(userInfos.firstName, userInfos.lastName);
4750
reset({
4851
[USER_NAME]: userInfos.sub,
52+
[USER_FULL_NAME]: fullName,
4953
[USER_PROFILE_NAME]: userInfos.profileName,
5054
[USER_SELECTED_GROUPS]: JSON.stringify(sortedGroups), // only used to dirty the form
5155
});
@@ -71,7 +75,11 @@ const UserModificationDialog: FunctionComponent<UserModificationDialogProps> = (
7175

7276
groupPromise
7377
.then((allGroups: GroupInfos[]) => {
74-
setGroupOptions(allGroups.map((g) => g.name).sort((a: string, b: string) => a.localeCompare(b)));
78+
setGroupOptions(
79+
allGroups
80+
.map((g): GroupSelectionItem => ({ name: g.name }))
81+
.sort((a: GroupSelectionItem, b: GroupSelectionItem) => a.name.localeCompare(b.name))
82+
);
7583
})
7684
.catch((error) => {
7785
snackError({

0 commit comments

Comments
 (0)