Skip to content

Commit a00e272

Browse files
committed
user dialog with group table, with style ok
Signed-off-by: David BRAQUART <[email protected]>
1 parent bc72bed commit a00e272

File tree

6 files changed

+204
-25
lines changed

6 files changed

+204
-25
lines changed
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/**
2+
* Copyright (c) 2024, RTE (http://www.rte-france.com)
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
6+
*/
7+
8+
import { FunctionComponent, useCallback, useMemo, useRef, useState } from 'react';
9+
import { FormattedMessage, useIntl } from 'react-intl';
10+
import { CustomAGGrid } from '@gridsuite/commons-ui';
11+
import { Grid, Typography } from '@mui/material';
12+
import { AgGridReact } from 'ag-grid-react';
13+
import { ColDef, GetRowIdParams, GridReadyEvent, RowSelectionOptions } from 'ag-grid-community';
14+
import { defaultColDef } from './table-config';
15+
16+
export interface TableSelectionProps {
17+
itemNameTranslationKey: string;
18+
tableItems: string[];
19+
tableSelectedItems?: string[];
20+
onSelectionChanged: (selectedItems: string[]) => void;
21+
}
22+
23+
const TableSelection: FunctionComponent<TableSelectionProps> = (props) => {
24+
const intl = useIntl();
25+
const [selectedRowsLength, setSelectedRowsLength] = useState(0);
26+
const gridRef = useRef<AgGridReact>(null);
27+
28+
const handleEquipmentSelectionChanged = useCallback(() => {
29+
const selectedRows = gridRef.current?.api.getSelectedRows();
30+
if (selectedRows == null) {
31+
setSelectedRowsLength(0);
32+
props.onSelectionChanged([]);
33+
} else {
34+
setSelectedRowsLength(selectedRows.length);
35+
props.onSelectionChanged(selectedRows.map((r) => r.id));
36+
}
37+
}, [props]);
38+
39+
const rowData = useMemo(() => {
40+
return props.tableItems.map((str) => ({ id: str }));
41+
}, [props.tableItems]);
42+
43+
const rowSelection: RowSelectionOptions = {
44+
mode: 'multiRow',
45+
enableClickSelection: false,
46+
checkboxes: true,
47+
headerCheckbox: true,
48+
hideDisabledCheckboxes: false,
49+
};
50+
51+
const columnDefs = useMemo(
52+
(): ColDef[] => [
53+
{
54+
field: 'id',
55+
filter: true,
56+
sortable: true,
57+
minWidth: 80,
58+
headerName: intl.formatMessage({
59+
id: props.itemNameTranslationKey,
60+
}),
61+
},
62+
],
63+
[intl, props.itemNameTranslationKey]
64+
);
65+
66+
function getRowId(params: GetRowIdParams): string {
67+
return params.data.id;
68+
}
69+
70+
const onGridReady = useCallback(
71+
({ api }: GridReadyEvent) => {
72+
api?.forEachNode((n) => {
73+
if (props.tableSelectedItems !== undefined && n.id && props.tableSelectedItems.includes(n.id)) {
74+
n.setSelected(true);
75+
}
76+
});
77+
},
78+
[props.tableSelectedItems]
79+
);
80+
81+
return (
82+
<Grid item container direction={'column'} style={{ height: '100%' }}>
83+
<Grid item>
84+
<Typography variant="subtitle1">
85+
<FormattedMessage id={props.itemNameTranslationKey}></FormattedMessage>
86+
{` (${selectedRowsLength} / ${rowData?.length ?? 0})`}
87+
</Typography>
88+
</Grid>
89+
<Grid item xs>
90+
<CustomAGGrid
91+
gridId="table-selection"
92+
ref={gridRef}
93+
rowData={rowData}
94+
columnDefs={columnDefs}
95+
defaultColDef={defaultColDef}
96+
getRowId={getRowId}
97+
rowSelection={rowSelection}
98+
onSelectionChanged={handleEquipmentSelectionChanged}
99+
onGridReady={onGridReady}
100+
/>
101+
</Grid>
102+
</Grid>
103+
);
104+
};
105+
export default TableSelection;

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ const GroupModificationDialog: FunctionComponent<GroupModificationDialogProps> =
3838
const [dataFetchStatus, setDataFetchStatus] = useState<string>(FetchStatus.IDLE);
3939

4040
useEffect(() => {
41-
// fetch available profiles
4241
if (groupInfos && open) {
4342
setDataFetchStatus(FetchStatus.FETCHING);
4443
UserAdminSrv.fetchUsers()

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

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

8-
import ProfileModificationForm, {
8+
import UserModificationForm, {
99
USER_NAME,
1010
USER_PROFILE_NAME,
11+
USER_SELECTED_GROUPS,
1112
UserModificationFormType,
1213
UserModificationSchema,
1314
} from './user-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 { UpdateUserInfos, UserAdminSrv, UserInfos, UserProfile } from '../../../services';
19+
import { GroupInfos, UpdateUserInfos, UserAdminSrv, UserInfos, UserProfile } from '../../../services';
1920

2021
interface UserModificationDialogProps {
2122
userInfos: UserInfos | undefined;
@@ -34,36 +35,64 @@ const UserModificationDialog: FunctionComponent<UserModificationDialogProps> = (
3435
const formMethods = useForm({
3536
resolver: yupResolver(UserModificationSchema),
3637
});
37-
const { reset } = formMethods;
38+
const { reset, setValue } = formMethods;
3839
const [profileOptions, setProfileOptions] = useState<string[]>([]);
40+
const [groupOptions, setGroupOptions] = useState<string[]>([]);
41+
const [selectedGroups, setSelectedGroups] = useState<string[]>([]);
3942
const [dataFetchStatus, setDataFetchStatus] = useState<string>(FetchStatus.IDLE);
4043

4144
useEffect(() => {
42-
// fetch available profiles
4345
if (userInfos && open) {
46+
setSelectedGroups(userInfos.groups);
47+
// fetch profile & groups
4448
setDataFetchStatus(FetchStatus.FETCHING);
45-
UserAdminSrv.fetchProfilesWithoutValidityCheck()
49+
let fetcherPromises: Promise<unknown>[] = [];
50+
const profilePromise = UserAdminSrv.fetchProfilesWithoutValidityCheck();
51+
const groupPromise = UserAdminSrv.fetchGroups();
52+
fetcherPromises.push(profilePromise);
53+
fetcherPromises.push(groupPromise);
54+
55+
profilePromise
4656
.then((allProfiles: UserProfile[]) => {
47-
setDataFetchStatus(FetchStatus.FETCH_SUCCESS);
4857
setProfileOptions(
4958
allProfiles.map((p) => p.name).sort((a: string, b: string) => a.localeCompare(b))
5059
);
5160
})
5261
.catch((error) => {
53-
setDataFetchStatus(FetchStatus.FETCH_ERROR);
5462
snackError({
5563
messageTxt: error.message,
5664
headerId: 'users.table.error.profiles',
5765
});
5866
});
67+
68+
groupPromise
69+
.then((allGroups: GroupInfos[]) => {
70+
setGroupOptions(allGroups.map((g) => g.name).sort((a: string, b: string) => a.localeCompare(b)));
71+
})
72+
.catch((error) => {
73+
snackError({
74+
messageTxt: error.message,
75+
headerId: 'users.table.error.groups',
76+
});
77+
});
78+
79+
Promise.all(fetcherPromises)
80+
.then(() => {
81+
setDataFetchStatus(FetchStatus.FETCH_SUCCESS);
82+
})
83+
.catch(() => {
84+
setDataFetchStatus(FetchStatus.FETCH_ERROR);
85+
});
5986
}
6087
}, [open, snackError, userInfos]);
6188

6289
useEffect(() => {
6390
if (userInfos && open) {
91+
const sortedGroups = Array.from(userInfos.groups).sort((a, b) => a.localeCompare(b));
6492
reset({
6593
[USER_NAME]: userInfos.sub,
6694
[USER_PROFILE_NAME]: userInfos.profileName,
95+
[USER_SELECTED_GROUPS]: JSON.stringify(sortedGroups), // only used to dirty the form
6796
});
6897
}
6998
}, [userInfos, open, reset]);
@@ -73,14 +102,27 @@ const UserModificationDialog: FunctionComponent<UserModificationDialogProps> = (
73102
onClose();
74103
}, [onClose]);
75104

105+
const onSelectionChanged = useCallback(
106+
(selectedItems: string[]) => {
107+
if (userInfos) {
108+
setSelectedGroups(selectedItems);
109+
selectedItems.sort((a, b) => a.localeCompare(b));
110+
setValue(USER_SELECTED_GROUPS, JSON.stringify(selectedItems), {
111+
shouldDirty: true,
112+
});
113+
}
114+
},
115+
[setValue, userInfos]
116+
);
117+
76118
const onSubmit = useCallback(
77119
(userFormData: UserModificationFormType) => {
78120
if (userInfos) {
79121
const newData: UpdateUserInfos = {
80122
sub: userInfos.sub, // sub cannot be changed, it is a PK in database
81123
isAdmin: userInfos.isAdmin, // cannot be changed for now
82124
profileName: userFormData.profileName ?? undefined,
83-
groups: [],
125+
groups: selectedGroups,
84126
};
85127
UserAdminSrv.udpateUser(newData)
86128
.catch((error) =>
@@ -94,7 +136,7 @@ const UserModificationDialog: FunctionComponent<UserModificationDialogProps> = (
94136
});
95137
}
96138
},
97-
[onUpdate, snackError, userInfos]
139+
[onUpdate, selectedGroups, snackError, userInfos]
98140
);
99141

100142
const isDataReady = useMemo(() => dataFetchStatus === FetchStatus.FETCH_SUCCESS, [dataFetchStatus]);
@@ -110,8 +152,16 @@ const UserModificationDialog: FunctionComponent<UserModificationDialogProps> = (
110152
titleId={'users.form.modification.title'}
111153
removeOptional={true}
112154
isDataFetching={isDataFetching}
155+
unscrollableFullHeight
113156
>
114-
{isDataReady && <ProfileModificationForm profileOptions={profileOptions} />}
157+
{isDataReady && (
158+
<UserModificationForm
159+
profileOptions={profileOptions}
160+
groupOptions={groupOptions}
161+
selectedGroups={selectedGroups}
162+
onSelectionChanged={onSelectionChanged}
163+
/>
164+
)}
115165
</CustomMuiDialog>
116166
);
117167
};

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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,38 @@ import { AutocompleteInput, TextInput } from '@gridsuite/commons-ui';
99
import Grid from '@mui/material/Grid';
1010
import React, { FunctionComponent } from 'react';
1111
import yup from '../../../utils/yup-config';
12+
import TableSelection from '../../common/table-selection';
1213

1314
export const USER_NAME = 'sub';
1415
export const USER_PROFILE_NAME = 'profileName';
16+
export const USER_SELECTED_GROUPS = 'groups';
1517

1618
export const UserModificationSchema = yup
1719
.object()
1820
.shape({
1921
[USER_NAME]: yup.string().trim().required('nameEmpty'),
2022
[USER_PROFILE_NAME]: yup.string().nullable(),
23+
[USER_SELECTED_GROUPS]: yup.string().nullable(),
2124
})
2225
.required();
2326

2427
export type UserModificationFormType = yup.InferType<typeof UserModificationSchema>;
2528

2629
interface UserModificationFormProps {
2730
profileOptions: string[];
31+
groupOptions: string[];
32+
selectedGroups?: string[];
33+
onSelectionChanged: (selectedItems: string[]) => void;
2834
}
2935

30-
const UserModificationForm: FunctionComponent<UserModificationFormProps> = ({ profileOptions }) => {
36+
const UserModificationForm: FunctionComponent<UserModificationFormProps> = ({
37+
profileOptions,
38+
groupOptions,
39+
selectedGroups,
40+
onSelectionChanged,
41+
}) => {
3142
return (
32-
<Grid container spacing={2} marginTop={'auto'}>
43+
<Grid item container spacing={2} marginTop={0} style={{ height: '100%' }}>
3344
<Grid item xs={12}>
3445
<TextInput
3546
name={USER_NAME}
@@ -50,6 +61,14 @@ const UserModificationForm: FunctionComponent<UserModificationFormProps> = ({ pr
5061
options={profileOptions}
5162
/>
5263
</Grid>
64+
<Grid item xs={12} style={{ height: '85%' }}>
65+
<TableSelection
66+
itemNameTranslationKey={'groups.form.field.group.label'}
67+
tableItems={groupOptions}
68+
tableSelectedItems={selectedGroups}
69+
onSelectionChanged={onSelectionChanged}
70+
/>
71+
</Grid>
5372
</Grid>
5473
);
5574
};

src/pages/users/users-page.tsx

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,24 @@ const UsersPage: FunctionComponent = () => {
4040
}, []);
4141

4242
return (
43-
<Grid item container direction="column" spacing={2} component="section">
44-
<Grid item container xs sx={{ width: 1 }}>
45-
<UserModificationDialog
46-
userInfos={editingUser}
47-
open={openUserModificationDialog}
48-
onClose={handleCloseUserModificationDialog}
49-
onUpdate={handleUpdateUserModificationDialog}
50-
/>
51-
<UsersTable gridRef={gridRef} onRowClicked={onRowClicked} setOpenAddUserDialog={setOpenAddUserDialog} />
52-
<AddUserDialog gridRef={gridRef} open={openAddUserDialog} setOpen={setOpenAddUserDialog} />
43+
<>
44+
<Grid item container direction="column" spacing={2} component="section">
45+
<Grid item container xs sx={{ width: 1 }}>
46+
<UsersTable
47+
gridRef={gridRef}
48+
onRowClicked={onRowClicked}
49+
setOpenAddUserDialog={setOpenAddUserDialog}
50+
/>
51+
</Grid>
5352
</Grid>
54-
</Grid>
53+
<AddUserDialog gridRef={gridRef} open={openAddUserDialog} setOpen={setOpenAddUserDialog} />
54+
<UserModificationDialog
55+
userInfos={editingUser}
56+
open={openUserModificationDialog}
57+
onClose={handleCloseUserModificationDialog}
58+
onUpdate={handleUpdateUserModificationDialog}
59+
/>
60+
</>
5561
);
5662
};
5763
export default UsersPage;

src/services/user-admin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export type UserInfos = {
4242
sub: string;
4343
profileName: string;
4444
isAdmin: boolean;
45-
groups: GroupInfos[];
45+
groups: string[];
4646
};
4747

4848
export function fetchUsers(): Promise<UserInfos[]> {

0 commit comments

Comments
 (0)