Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 75 additions & 51 deletions src/pages/common/table-selection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,96 +5,120 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

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

export interface TableSelectionProps {
itemName: string;
tableItems: string[];
tableSelectedItems?: string[];
onSelectionChanged: (selectedItems: string[]) => void;
/**
* Generic props for TableSelection component.
* @template TData - The type of data items in the table
*/
export interface TableSelectionProps<TData> {
/** Translation key for the table title */
titleId: string;
/** Array of data items to display */
items: TData[];
/** Function to extract the unique ID from each item (used for selection tracking) */
getItemId: (item: TData) => string;
/** Column definitions for the AG Grid */
columnDefs: ColDef<TData>[];
/** Array of selected item IDs */
selectedIds?: string[];
/** Callback when selection changes, receives array of selected IDs */
onSelectionChanged: (selectedIds: string[]) => void;
}

const rowSelection = {
...defaultRowSelection,
headerCheckbox: false,
};

const TableSelection: FunctionComponent<TableSelectionProps> = (props) => {
const [selectedRowsLength, setSelectedRowsLength] = useState(0);
const gridRef = useRef<AgGridReact>(null);
/**
* A generic table component with row selection support.
* Displays data in an AG Grid with checkboxes for selection.
*/
function TableSelection<TData>({
titleId,
items,
getItemId,
columnDefs,
selectedIds,
onSelectionChanged,
}: Readonly<TableSelectionProps<TData>>) {
const [selectedCount, setSelectedCount] = useState(0);
const gridRef = useRef<AgGridReact<TData>>(null);

const getRowId = useCallback(
(params: GetRowIdParams<TData>): string => {
return getItemId(params.data);
},
[getItemId]
);

const handleEquipmentSelectionChanged = useCallback(() => {
const handleSelectionChanged = useCallback(() => {
const selectedRows = gridRef.current?.api.getSelectedRows();
if (selectedRows == null) {
setSelectedRowsLength(0);
props.onSelectionChanged([]);
setSelectedCount(0);
onSelectionChanged([]);
} else {
setSelectedRowsLength(selectedRows.length);
props.onSelectionChanged(selectedRows.map((r) => r.id));
setSelectedCount(selectedRows.length);
onSelectionChanged(selectedRows.map(getItemId));
}
}, [props]);

const rowData = useMemo(() => {
return props.tableItems.map((str) => ({ id: str }));
}, [props.tableItems]);
}, [onSelectionChanged, getItemId]);

const columnDefs = useMemo(
(): ColDef[] => [
{
field: 'id',
filter: true,
initialSort: 'asc',
tooltipField: 'id',
flex: 1,
},
],
[]
);

function getRowId(params: GetRowIdParams): string {
return params.data.id;
}

const onGridReady = useCallback(
({ api }: GridReadyEvent) => {
api?.forEachNode((n) => {
if (props.tableSelectedItems !== undefined && n.id && props.tableSelectedItems.includes(n.id)) {
n.setSelected(true);
const handleGridReady = useCallback(
({ api }: GridReadyEvent<TData>) => {
if (!selectedIds?.length) {
return;
}
api.forEachNode((node) => {
const nodeId = node.id;
if (nodeId && selectedIds.includes(nodeId)) {
node.setSelected(true);
}
});
},
[props.tableSelectedItems]
[selectedIds]
);

const mergedColumnDefs = useMemo((): ColDef<TData>[] => {
return columnDefs.map((col, index) => ({
filter: true,
flex: 1,
...col,
// First column gets initial sort if not specified elsewhere
...(index === 0 && !columnDefs.some((c) => c.initialSort) ? { initialSort: 'asc' as const } : {}),
}));
}, [columnDefs]);

return (
<Grid item container direction={'column'} style={{ height: '100%' }}>
<Grid item container direction="column" style={{ height: '100%' }}>
<Grid item>
<Typography variant="subtitle1">
<FormattedMessage id={props.itemName}></FormattedMessage>
{` (${selectedRowsLength} / ${rowData?.length ?? 0})`}
<FormattedMessage id={titleId} />
{` (${selectedCount} / ${items.length})`}
</Typography>
</Grid>
<Grid item xs>
<CustomAGGrid
gridId="table-selection"
ref={gridRef}
rowData={rowData}
columnDefs={columnDefs}
rowData={items}
columnDefs={mergedColumnDefs}
defaultColDef={defaultColDef}
rowSelection={rowSelection}
getRowId={getRowId}
onSelectionChanged={handleEquipmentSelectionChanged}
onGridReady={onGridReady}
accentedSort={true}
onSelectionChanged={handleSelectionChanged}
onGridReady={handleGridReady}
accentedSort
/>
</Grid>
</Grid>
);
};
}

export default TableSelection;
14 changes: 11 additions & 3 deletions src/pages/groups/modification/group-modification-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ import GroupModificationForm, {
GroupModificationFormType,
GroupModificationSchema,
SELECTED_USERS,
UserSelectionItem,
} from './group-modification-form';
import { yupResolver } from '@hookform/resolvers/yup';
import { useForm } from 'react-hook-form';
import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react';
import { CustomMuiDialog, FetchStatus, useSnackMessage } from '@gridsuite/commons-ui';
import { GroupInfos, UserAdminSrv, UserInfos } from '../../../services';
import { formatFullName, GroupInfos, UserAdminSrv, UserInfos } from '../../../services';

interface GroupModificationDialogProps {
groupInfos: GroupInfos | undefined;
Expand All @@ -35,7 +36,7 @@ const GroupModificationDialog: FunctionComponent<GroupModificationDialogProps> =
resolver: yupResolver(GroupModificationSchema),
});
const { reset, setValue } = formMethods;
const [userOptions, setUserOptions] = useState<string[]>([]);
const [userOptions, setUserOptions] = useState<UserSelectionItem[]>([]);
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
const [dataFetchStatus, setDataFetchStatus] = useState<string>(FetchStatus.IDLE);

Expand All @@ -54,7 +55,14 @@ const GroupModificationDialog: FunctionComponent<GroupModificationDialogProps> =
.then((allUsers: UserInfos[]) => {
setDataFetchStatus(FetchStatus.FETCH_SUCCESS);
setUserOptions(
allUsers?.map((p) => p.sub).sort((a: string, b: string) => a.localeCompare(b)) || []
allUsers
?.map(
(p): UserSelectionItem => ({
sub: p.sub,
fullName: formatFullName(p.firstName, p.lastName),
})
)
.sort((a, b) => a.sub.localeCompare(b.sub)) || []
);
})
.catch((error) => {
Expand Down
38 changes: 32 additions & 6 deletions src/pages/groups/modification/group-modification-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

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

export const GROUP_NAME = 'name';
export const SELECTED_USERS = 'users';
Expand All @@ -22,9 +24,13 @@ export const GroupModificationSchema = yup
.required();

export type GroupModificationFormType = yup.InferType<typeof GroupModificationSchema>;
export interface UserSelectionItem {
sub: string;
fullName: string;
}

interface GroupModificationFormProps {
usersOptions: string[];
usersOptions: UserSelectionItem[];
selectedUsers?: string[];
onSelectionChanged: (selectedItems: string[]) => void;
}
Expand All @@ -34,16 +40,36 @@ const GroupModificationForm: FunctionComponent<GroupModificationFormProps> = ({
selectedUsers,
onSelectionChanged,
}) => {
const intl = useIntl();

const userColumnDefs = useMemo(
(): ColDef<UserSelectionItem>[] => [
{
field: 'sub',
headerName: intl.formatMessage({ id: 'users.table.id' }),
tooltipField: 'sub',
},
{
field: 'fullName',
headerName: intl.formatMessage({ id: 'users.table.fullName' }),
tooltipField: 'fullName',
},
],
[intl]
);

return (
<Grid item container spacing={2} marginTop={0} style={{ height: '100%' }}>
<Grid item xs={12}>
<TextInput name={GROUP_NAME} label={'groups.table.id'} clearable={true} />
</Grid>
<Grid item xs={12} style={{ height: '90%' }}>
<TableSelection
itemName={'groups.table.users'}
tableItems={usersOptions}
tableSelectedItems={selectedUsers}
<TableSelection<UserSelectionItem>
titleId="groups.table.users"
items={usersOptions}
getItemId={(user) => user.sub}
columnDefs={userColumnDefs}
selectedIds={selectedUsers}
onSelectionChanged={onSelectionChanged}
/>
</Grid>
Expand Down
14 changes: 11 additions & 3 deletions src/pages/users/modification/user-modification-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
*/

import UserModificationForm, {
GroupSelectionItem,
USER_FULL_NAME,
USER_NAME,
USER_PROFILE_NAME,
USER_SELECTED_GROUPS,
Expand All @@ -16,7 +18,7 @@ import { yupResolver } from '@hookform/resolvers/yup';
import { useForm } from 'react-hook-form';
import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react';
import { CustomMuiDialog, FetchStatus, useSnackMessage } from '@gridsuite/commons-ui';
import { GroupInfos, UserAdminSrv, UserInfos, UserProfile } from '../../../services';
import { formatFullName, GroupInfos, UserAdminSrv, UserInfos, UserProfile } from '../../../services';

interface UserModificationDialogProps {
userInfos: UserInfos | undefined;
Expand All @@ -37,15 +39,17 @@ const UserModificationDialog: FunctionComponent<UserModificationDialogProps> = (
});
const { reset, setValue } = formMethods;
const [profileOptions, setProfileOptions] = useState<string[]>([]);
const [groupOptions, setGroupOptions] = useState<string[]>([]);
const [groupOptions, setGroupOptions] = useState<GroupSelectionItem[]>([]);
const [selectedGroups, setSelectedGroups] = useState<string[]>([]);
const [dataFetchStatus, setDataFetchStatus] = useState<string>(FetchStatus.IDLE);

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

groupPromise
.then((allGroups: GroupInfos[]) => {
setGroupOptions(allGroups.map((g) => g.name).sort((a: string, b: string) => a.localeCompare(b)));
setGroupOptions(
allGroups
.map((g): GroupSelectionItem => ({ name: g.name }))
.sort((a: GroupSelectionItem, b: GroupSelectionItem) => a.name.localeCompare(b.name))
);
})
.catch((error) => {
snackError({
Expand Down
Loading
Loading