@deix/rossini-identity-management 5.2.0 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,7 +12,9 @@ interface UsersProps {
12
12
  emailAsUsername?: boolean;
13
13
  hideGroupsSelection?: boolean;
14
14
  disableDelete?: boolean;
15
+ showGroupsColumn?: boolean;
16
+ showExport?: boolean;
15
17
  }
16
- declare const Users: ({ apiUrl, additionalTabs, onUserCreate, onUserUpdate, onUserDelete, editUsername, filteredGroup, emailAsUsername, hideGroupsSelection, disableDelete, }: UsersProps) => React.JSX.Element;
18
+ declare const Users: ({ apiUrl, additionalTabs, onUserCreate, onUserUpdate, onUserDelete, editUsername, filteredGroup, emailAsUsername, hideGroupsSelection, disableDelete, showGroupsColumn, showExport, }: UsersProps) => React.JSX.Element;
17
19
  export default Users;
18
20
  //# sourceMappingURL=Users.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Users.d.ts","sourceRoot":"","sources":["../../../src/components/users/Users.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAYnD,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAEzC,OAAiB,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAKhD,UAAU,UAAU;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,OAAO,EAAE,CAAC;IAE3B,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,KAAK,IAAI,CAAC;IAEtD,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,IAAI,KAAK,IAAI,CAAC;IAEvC,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IAEpC,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAE9B,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,QAAA,MAAM,KAAK,4JAWR,UAAU,sBAyKZ,CAAC;eAEa,KAAK"}
1
+ {"version":3,"file":"Users.d.ts","sourceRoot":"","sources":["../../../src/components/users/Users.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAuC,MAAM,OAAO,CAAC;AAuB5D,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAQzC,OAAiB,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAShD,UAAU,UAAU;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,OAAO,EAAE,CAAC;IAE3B,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,KAAK,IAAI,CAAC;IAEtD,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,IAAI,KAAK,IAAI,CAAC;IAEvC,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IAEpC,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAE9B,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,QAAA,MAAM,KAAK,0LAaR,UAAU,sBA+RZ,CAAC;eAEa,KAAK"}
@@ -1,27 +1,59 @@
1
1
  'use client';
2
2
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useEffect, useState } from 'react';
3
+ import { useEffect, useMemo, useState } from 'react';
4
4
  import PersonAddIcon from '@mui/icons-material/PersonAdd';
5
- import { Box, Button, Grid, IconButton, Link } from '@mui/material';
5
+ import FileDownloadIcon from '@mui/icons-material/FileDownload';
6
+ import { Box, Button, Checkbox, Grid, IconButton, Link, ListItemText, MenuItem, TextField, } from '@mui/material';
6
7
  import ArrowBackIcon from '@mui/icons-material/ArrowBack';
7
8
  import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';
8
9
  import { useAPI } from '@deix/rossini-core';
9
10
  import { useQueryClient } from '@tanstack/react-query';
11
+ import { useGridApiRef } from '@mui/x-data-grid';
10
12
  import { SearchField } from '../../components';
11
13
  import CreateUserDialog from './CreateUserDialog';
12
14
  import UsersTable from './UsersTable';
13
15
  import usersTranslations from '../../lang/users';
14
- import { useDeleteUser, useUsers } from '../../utils/hooks/users';
16
+ import { useDeleteUser, useGroupsByUser, useUsers, useUsersInGroups, } from '../../utils/hooks/users';
17
+ import { useGroups } from '../../utils/hooks/groups';
15
18
  import UserTabs from './UsersTabs';
16
19
  import { APIClientProvider } from '../../utils/hooks/useAPIClient';
17
20
  import commonTranslations from '../../lang/common';
18
21
  import useTranslate from '../../utils/hooks/useTranslate';
19
- const Users = ({ apiUrl, additionalTabs = [], onUserCreate, onUserUpdate, onUserDelete, editUsername, filteredGroup, emailAsUsername = false, hideGroupsSelection = false, disableDelete = false, }) => {
22
+ import { sortGroupsByName } from '../../utils/helpers';
23
+ // Group filter value for the users that are not in any group
24
+ const NO_GROUP = '__no-group__';
25
+ const Users = ({ apiUrl, additionalTabs = [], onUserCreate, onUserUpdate, onUserDelete, editUsername, filteredGroup, emailAsUsername = false, hideGroupsSelection = false, disableDelete = false, showGroupsColumn = false, showExport = false, }) => {
20
26
  const t = useTranslate();
21
27
  const apiClient = useAPI(apiUrl);
22
28
  const queryClient = useQueryClient();
23
- const { data: users } = useUsers(apiClient, filteredGroup);
29
+ // Values selected in the group filter: group ids and/or NO_GROUP.
30
+ // Empty means all users; otherwise a user is shown if it matches any of them.
31
+ const [groupFilter, setGroupFilter] = useState([]);
32
+ const withoutGroup = groupFilter.includes(NO_GROUP);
33
+ const { data: groups } = useGroups(apiClient);
34
+ // Group filter and groups column are hidden when there are no groups
35
+ const hasGroups = !!groups && groups.length > 0;
36
+ const groupsColumnVisible = showGroupsColumn && hasGroups;
37
+ // Passing id and name avoids looking the groups up by name
38
+ const selectedGroups = useMemo(() => groups?.filter((g) => groupFilter.includes(g.id)) ?? [], [groups, groupFilter]);
39
+ const { data: allUsers } = useUsers(apiClient, filteredGroup);
40
+ const { data: groupsUsers } = useUsersInGroups(apiClient, selectedGroups);
41
+ // Needed by the groups column and to find the users without group
42
+ const { data: groupsByUser } = useGroupsByUser(apiClient, groupsColumnVisible || withoutGroup);
43
+ const users = useMemo(() => {
44
+ if (groupFilter.length === 0)
45
+ return allUsers;
46
+ if (!groupsUsers || (withoutGroup && (!allUsers || !groupsByUser))) {
47
+ return undefined;
48
+ }
49
+ if (!withoutGroup)
50
+ return groupsUsers;
51
+ // The two lists are disjoint: users without group are in no group
52
+ const usersWithoutGroup = allUsers?.filter((u) => !groupsByUser?.[u.id]) ?? [];
53
+ return [...groupsUsers, ...usersWithoutGroup];
54
+ }, [groupFilter, allUsers, groupsUsers, withoutGroup, groupsByUser]);
24
55
  const { mutate: deleteUser } = useDeleteUser(queryClient, apiClient);
56
+ const apiRef = useGridApiRef();
25
57
  const [searchString, setSearchString] = useState('');
26
58
  const [filteredUsers, setFilteredUsers] = useState();
27
59
  const [selectedUser, setSelectedUser] = useState();
@@ -46,10 +78,12 @@ const Users = ({ apiUrl, additionalTabs = [], onUserCreate, onUserUpdate, onUser
46
78
  .includes(searchString.toLowerCase())));
47
79
  }
48
80
  }, [searchString, users, setFilteredUsers]);
49
- // If the user change updated the selected user to reflect the chages
81
+ // If the user change updated the selected user to reflect the chages.
82
+ // Uses allUsers so that removing the user from the filtered group
83
+ // does not close its details.
50
84
  useEffect(() => {
51
85
  if (selectedUser) {
52
- const updatedSelectedUser = users?.find((user) => user.id === selectedUser.id);
86
+ const updatedSelectedUser = allUsers?.find((user) => user.id === selectedUser.id);
53
87
  if (updatedSelectedUser) {
54
88
  setSelectedUser(updatedSelectedUser);
55
89
  }
@@ -58,7 +92,16 @@ const Users = ({ apiUrl, additionalTabs = [], onUserCreate, onUserUpdate, onUser
58
92
  setSelectedUser(undefined);
59
93
  }
60
94
  }
61
- }, [users, selectedUser, setSelectedUser]);
95
+ }, [allUsers, selectedUser, setSelectedUser]);
96
+ // Downloads the users in the table (all pages, with search and group
97
+ // filters applied) as e.g. "Utenti-2026-10-30.csv"
98
+ const handleExport = () => {
99
+ apiRef.current?.exportDataAsCsv({
100
+ fileName: `${t(usersTranslations.title)}-${new Date().toISOString().slice(0, 10)}`,
101
+ // The BOM lets Excel read accented characters correctly
102
+ utf8WithBom: true,
103
+ });
104
+ };
62
105
  const handleDelete = (user) => {
63
106
  deleteUser(user.id);
64
107
  if (typeof onUserDelete === 'function') {
@@ -76,12 +119,35 @@ const Users = ({ apiUrl, additionalTabs = [], onUserCreate, onUserUpdate, onUser
76
119
  }, children: [!selectedUser && (_jsxs(_Fragment, { children: [_jsxs(Grid, { container: true, spacing: 2, sx: { marginBottom: 2 }, children: [_jsx(Grid, { children: _jsx(SearchField, { placeholder: t(usersTranslations.search), onSearch: (value) => {
77
120
  setSearchString(value);
78
121
  setPaginationModel((model) => ({ ...model, page: 0 }));
79
- } }) }), _jsx(Grid, { children: _jsx(Button, { variant: 'contained', startIcon: _jsx(PersonAddIcon, {}), onClick: () => {
122
+ } }) }), !filteredGroup && hasGroups && (_jsx(Grid, { children: _jsxs(TextField, { select: true, size: 'small', label: t(usersTranslations.groupFilter), value: groupFilter, onChange: (evt) => {
123
+ const { value } = evt.target;
124
+ // On autofill the value is a comma separated string
125
+ setGroupFilter(typeof value === 'string' ? value.split(',') : value);
126
+ setPaginationModel((model) => ({ ...model, page: 0 }));
127
+ }, slotProps: {
128
+ select: {
129
+ multiple: true,
130
+ // Show "All groups" instead of an empty select
131
+ displayEmpty: true,
132
+ renderValue: (selected) => {
133
+ const values = selected;
134
+ if (values.length === 0) {
135
+ return t(usersTranslations.allGroups);
136
+ }
137
+ return values
138
+ .map((v) => v === NO_GROUP
139
+ ? t(usersTranslations.noGroup)
140
+ : groups.find((g) => g.id === v)?.name)
141
+ .join(', ');
142
+ },
143
+ },
144
+ inputLabel: { shrink: true },
145
+ }, sx: { minWidth: 200, maxWidth: 400 }, children: [_jsxs(MenuItem, { value: NO_GROUP, children: [_jsx(Checkbox, { size: 'small', checked: groupFilter.includes(NO_GROUP) }), _jsx(ListItemText, { primary: t(usersTranslations.noGroup) })] }), sortGroupsByName(groups).map((group) => (_jsxs(MenuItem, { value: group.id, children: [_jsx(Checkbox, { size: 'small', checked: groupFilter.includes(group.id) }), _jsx(ListItemText, { primary: group.name })] }, group.id)))] }) })), _jsx(Grid, { children: _jsx(Button, { variant: 'contained', startIcon: _jsx(PersonAddIcon, {}), onClick: () => {
80
146
  setOpenCreateUserDialog(true);
81
147
  setSelectedUser(undefined);
82
- }, children: t(usersTranslations.new) }) })] }), _jsx(UsersTable, { users: filteredUsers, selectedUser: selectedUser, setSelectedUser: (user) => {
148
+ }, children: t(usersTranslations.new) }) }), showExport && (_jsx(Grid, { sx: { marginLeft: 'auto' }, children: _jsx(Button, { variant: 'outlined', startIcon: _jsx(FileDownloadIcon, {}), onClick: handleExport, children: t(usersTranslations.download) }) }))] }), _jsx(UsersTable, { users: filteredUsers, selectedUser: selectedUser, setSelectedUser: (user) => {
83
149
  setSelectedUser(user);
84
- }, onDelete: handleDelete, emailAsUsername: emailAsUsername, disableDelete: disableDelete, paginationModel: paginationModel, onPaginationModelChange: setPaginationModel })] })), selectedUser && (_jsxs(Box, { sx: {
150
+ }, onDelete: handleDelete, emailAsUsername: emailAsUsername, disableDelete: disableDelete, showGroupsColumn: groupsColumnVisible, groupsByUser: groupsByUser, apiRef: apiRef, paginationModel: paginationModel, onPaginationModelChange: setPaginationModel })] })), selectedUser && (_jsxs(Box, { sx: {
85
151
  display: 'flex',
86
152
  flexDirection: 'column',
87
153
  alignItems: 'flex-start',
@@ -1,18 +1,20 @@
1
- import React from 'react';
2
- import { GridPaginationModel } from '@mui/x-data-grid';
1
+ import { RefObject } from 'react';
2
+ import { GridApi, GridPaginationModel } from '@mui/x-data-grid';
3
3
  import { Group } from '../../types/groups';
4
4
  import { User } from '../../types/users';
5
5
  export interface TableProps {
6
6
  users?: User[];
7
- groups?: Group[];
8
7
  selectedUser?: User;
9
8
  setSelectedUser: (user: User) => void;
10
9
  onDelete: (user: User) => void;
11
10
  emailAsUsername?: boolean;
12
11
  disableDelete?: boolean;
12
+ showGroupsColumn?: boolean;
13
+ groupsByUser?: Record<string, Group[]>;
14
+ apiRef?: RefObject<GridApi | null>;
13
15
  paginationModel?: GridPaginationModel;
14
16
  onPaginationModelChange?: (model: GridPaginationModel) => void;
15
17
  }
16
- declare const UsersTable: ({ users, selectedUser, setSelectedUser, onDelete, emailAsUsername, disableDelete, paginationModel, onPaginationModelChange, }: TableProps) => React.JSX.Element;
18
+ declare const UsersTable: ({ users, selectedUser, setSelectedUser, onDelete, emailAsUsername, disableDelete, showGroupsColumn, groupsByUser, apiRef, paginationModel, onPaginationModelChange, }: TableProps) => import("react").JSX.Element;
17
19
  export default UsersTable;
18
20
  //# sourceMappingURL=UsersTable.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"UsersTable.d.ts","sourceRoot":"","sources":["../../../src/components/users/UsersTable.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAyBzC,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC;IACf,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC;IACjB,YAAY,CAAC,EAAE,IAAI,CAAC;IACpB,eAAe,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IACtC,QAAQ,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IAC/B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,eAAe,CAAC,EAAE,mBAAmB,CAAC;IACtC,uBAAuB,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,IAAI,CAAC;CAChE;AAED,QAAA,MAAM,UAAU,kIASb,UAAU,sBAYZ,CAAC;eAEa,UAAU"}
1
+ {"version":3,"file":"UsersTable.d.ts","sourceRoot":"","sources":["../../../src/components/users/UsersTable.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAElC,OAAO,EAAE,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAGhE,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAgDzC,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC;IACf,YAAY,CAAC,EAAE,IAAI,CAAC;IACpB,eAAe,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IACtC,QAAQ,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IAC/B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;IAEvC,MAAM,CAAC,EAAE,SAAS,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IACnC,eAAe,CAAC,EAAE,mBAAmB,CAAC;IACtC,uBAAuB,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,IAAI,CAAC;CAChE;AAED,QAAA,MAAM,UAAU,0KAYb,UAAU,gCAgCZ,CAAC;eAEa,UAAU"}
@@ -1,6 +1,10 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Chip, Tooltip } from '@mui/material';
2
3
  import usersTranslations from '../../lang/users';
4
+ import { sortGroupsByName } from '../../utils/helpers';
3
5
  import Table from '../tables/Table';
6
+ // Groups shown as chips in a cell; the others are summarized in a "+N" chip
7
+ const MAX_GROUP_CHIPS = 2;
4
8
  const usersTableColumns = [
5
9
  {
6
10
  field: 'firstName',
@@ -21,5 +25,26 @@ const usersTableColumns = [
21
25
  type: 'boolean',
22
26
  },
23
27
  ];
24
- const UsersTable = ({ users = [], selectedUser, setSelectedUser, onDelete, emailAsUsername = false, disableDelete = false, paginationModel, onPaginationModelChange, }) => (_jsx(Table, { rows: users, columns: usersTableColumns.filter((c) => c.field !== 'username' || !emailAsUsername), selectedRowId: selectedUser?.id, onRowClick: setSelectedUser, onDelete: disableDelete ? undefined : onDelete, paginationModel: paginationModel, onPaginationModelChange: onPaginationModelChange }));
28
+ const GroupChips = ({ groups = [] }) => {
29
+ const sortedGroups = sortGroupsByName(groups);
30
+ const hiddenGroups = sortedGroups.slice(MAX_GROUP_CHIPS);
31
+ return (_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 0.5, height: '100%' }, children: [sortedGroups.slice(0, MAX_GROUP_CHIPS).map((group) => (_jsx(Chip, { label: group.name, size: 'small' }, group.id))), hiddenGroups.length > 0 && (_jsx(Tooltip, { title: hiddenGroups.map((g) => g.name).join(', '), children: _jsx(Chip, { label: `+${hiddenGroups.length}`, size: 'small' }) }))] }));
32
+ };
33
+ const UsersTable = ({ users = [], selectedUser, setSelectedUser, onDelete, emailAsUsername = false, disableDelete = false, showGroupsColumn = false, groupsByUser, apiRef, paginationModel, onPaginationModelChange, }) => (_jsx(Table, { rows: users, columns: [
34
+ ...usersTableColumns.filter((c) => c.field !== 'username' || !emailAsUsername),
35
+ ...(showGroupsColumn
36
+ ? [
37
+ {
38
+ field: 'groups',
39
+ label: usersTranslations.userGroups,
40
+ sortable: false,
41
+ // Used by the CSV export, where the chips are not rendered
42
+ valueGetter: (user) => sortGroupsByName(groupsByUser?.[user.id] ?? [])
43
+ .map((g) => g.name)
44
+ .join('; '),
45
+ renderCell: (user) => (_jsx(GroupChips, { groups: groupsByUser?.[user.id] })),
46
+ },
47
+ ]
48
+ : []),
49
+ ], selectedRowId: selectedUser?.id, onRowClick: setSelectedUser, onDelete: disableDelete ? undefined : onDelete, paginationModel: paginationModel, onPaginationModelChange: onPaginationModelChange, apiRef: apiRef }));
25
50
  export default UsersTable;
package/lib/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { default as Users } from './components/users/Users';
2
2
  export { default as Groups } from './components/groups/Groups';
3
- export type { User, KeycloakQueryParams, UserByIdResult, UseUsersByIdsOptions, } from './types/users';
4
- export type { Group } from './types/groups';
3
+ export type { User, KeycloakQueryParams, UserByIdResult, UsersInGroupsResult, UseUsersByIdsOptions, } from './types/users';
4
+ export type { Group, GroupFilter } from './types/groups';
5
5
  export { default as SearchField } from './components/SearchField';
6
6
  export { default as DeleteConfirmationDialog } from './components/DeleteConfirmationDialog';
7
7
  export { default as AddItemDialog } from './components/AddItemDialog';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAC/D,YAAY,EACV,IAAI,EACJ,mBAAmB,EACnB,cAAc,EACd,oBAAoB,GACrB,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,OAAO,IAAI,wBAAwB,EAAE,MAAM,uCAAuC,CAAC;AAC5F,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,4BAA4B,CAAC;AACtE,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,4BAA4B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAC/D,YAAY,EACV,IAAI,EACJ,mBAAmB,EACnB,cAAc,EACd,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,OAAO,IAAI,wBAAwB,EAAE,MAAM,uCAAuC,CAAC;AAC5F,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,4BAA4B,CAAC;AACtE,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,4BAA4B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../src/lang/users.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEvD,QAAA,MAAM,iBAAiB,EA6IlB;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,CAAA;CAAE,CAAC;eAE3B,iBAAiB"}
1
+ {"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../src/lang/users.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEvD,QAAA,MAAM,iBAAiB,EA6JlB;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,CAAA;CAAE,CAAC;eAE3B,iBAAiB"}
package/lib/lang/users.js CHANGED
@@ -7,6 +7,22 @@ const usersTranslations = {
7
7
  en: 'Search...',
8
8
  it: 'Cerca...',
9
9
  },
10
+ groupFilter: {
11
+ en: 'Group',
12
+ it: 'Gruppo',
13
+ },
14
+ allGroups: {
15
+ en: 'All groups',
16
+ it: 'Tutti i gruppi',
17
+ },
18
+ noGroup: {
19
+ en: 'No group',
20
+ it: 'Nessun gruppo',
21
+ },
22
+ download: {
23
+ en: 'Download CSV',
24
+ it: 'Scarica CSV',
25
+ },
10
26
  new: {
11
27
  en: 'New User',
12
28
  it: 'Nuovo Utente',
@@ -2,4 +2,8 @@ export interface Group {
2
2
  id: string;
3
3
  name: string;
4
4
  }
5
+ export interface GroupFilter {
6
+ name: string;
7
+ id?: string;
8
+ }
5
9
  //# sourceMappingURL=groups.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"groups.d.ts","sourceRoot":"","sources":["../../src/types/groups.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd"}
1
+ {"version":3,"file":"groups.d.ts","sourceRoot":"","sources":["../../src/types/groups.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AAID,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,CAAC,EAAE,MAAM,CAAC;CACb"}
@@ -17,6 +17,11 @@ export interface UserByIdResult {
17
17
  isLoading: boolean;
18
18
  isError: boolean;
19
19
  }
20
+ export interface UsersInGroupsResult {
21
+ data?: User[];
22
+ isLoading: boolean;
23
+ isError: boolean;
24
+ }
20
25
  export interface UseUsersByIdsOptions {
21
26
  /** Milliseconds a cached user is considered fresh. Default: 60000. */
22
27
  staleTime?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../src/types/users.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,MAAM,WAAW,mBAAmB;IAClC,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AASD,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iEAAiE;IACjE,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;CAC1B"}
1
+ {"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../src/types/users.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,MAAM,WAAW,mBAAmB;IAClC,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AASD,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;CAClB;AAID,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC;IACd,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iEAAiE;IACjE,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;CAC1B"}
@@ -1 +1,3 @@
1
+ import { Group } from '../../types/groups';
2
+ export declare const sortGroupsByName: (groups: Group[]) => Group[];
1
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/utils/helpers/index.ts"],"names":[],"mappings":""}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/utils/helpers/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAG3C,eAAO,MAAM,gBAAgB,WAAY,KAAK,EAAE,YACa,CAAC"}
@@ -1 +1,2 @@
1
- "use strict";
1
+ // Returns a copy of the groups sorted by name
2
+ export const sortGroupsByName = (groups) => groups.slice().sort((a, b) => a.name.localeCompare(b.name));
@@ -2,6 +2,7 @@ import { QueryClient, UseMutationResult, UseQueryResult } from '@tanstack/react-
2
2
  import { AxiosInstance } from 'axios';
3
3
  import { Group } from '../../types/groups';
4
4
  import { User } from '../../types/users';
5
+ export declare const getGroups: (apiClient: AxiosInstance) => Promise<Group[]>;
5
6
  export declare const useGroups: (apiClient: AxiosInstance) => UseQueryResult<Group[] | undefined, Error>;
6
7
  export declare const useAddGroup: (queryClient: QueryClient, apiClient: AxiosInstance) => UseMutationResult<Group, Error, Group, unknown>;
7
8
  export declare const useUpdateGroup: (queryClient: QueryClient, apiClient: AxiosInstance) => UseMutationResult<Group, Error, Group, unknown>;
@@ -1 +1 @@
1
- {"version":3,"file":"groups.d.ts","sourceRoot":"","sources":["../../../src/utils/hooks/groups.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EACX,iBAAiB,EACjB,cAAc,EAGf,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAGtC,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAE3C,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAWzC,eAAO,MAAM,SAAS,cACT,aAAa,KACvB,cAAc,CAAC,KAAK,EAAE,GAAG,SAAS,EAAE,KAAK,CAM3C,CAAC;AAcF,eAAO,MAAM,WAAW,gBACT,WAAW,aACb,aAAa,oDA6BzB,CAAC;AAiBF,eAAO,MAAM,cAAc,gBACZ,WAAW,aACb,aAAa,oDA8BzB,CAAC;AAgBF,eAAO,MAAM,cAAc,gBACZ,WAAW,aACb,aAAa,6EA6BzB,CAAC;AAgBF,eAAO,MAAM,aAAa,OACpB,MAAM,GAAG,SAAS,aACX,aAAa,KACvB,cAAc,CAAC,IAAI,EAAE,GAAG,SAAS,EAAE,KAAK,CAM1C,CAAC"}
1
+ {"version":3,"file":"groups.d.ts","sourceRoot":"","sources":["../../../src/utils/hooks/groups.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EACX,iBAAiB,EACjB,cAAc,EAGf,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAGtC,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAE3C,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAIzC,eAAO,MAAM,SAAS,cAAqB,aAAa,qBAKvD,CAAC;AAEF,eAAO,MAAM,SAAS,cACT,aAAa,KACvB,cAAc,CAAC,KAAK,EAAE,GAAG,SAAS,EAAE,KAAK,CAM3C,CAAC;AAcF,eAAO,MAAM,WAAW,gBACT,WAAW,aACb,aAAa,oDA6BzB,CAAC;AAiBF,eAAO,MAAM,cAAc,gBACZ,WAAW,aACb,aAAa,oDA8BzB,CAAC;AAgBF,eAAO,MAAM,cAAc,gBACZ,WAAW,aACb,aAAa,6EA6BzB,CAAC;AAgBF,eAAO,MAAM,aAAa,OACpB,MAAM,GAAG,SAAS,aACX,aAAa,KACvB,cAAc,CAAC,IAAI,EAAE,GAAG,SAAS,EAAE,KAAK,CAM1C,CAAC"}
@@ -3,7 +3,7 @@ import { enqueueSnackbar } from 'notistack';
3
3
  import groupsTranslations from '../../lang/groups';
4
4
  import useTranslate from './useTranslate';
5
5
  // Get list of groups
6
- const getGroups = async (apiClient) => {
6
+ export const getGroups = async (apiClient) => {
7
7
  const { data } = await apiClient.get(`/groups`, {
8
8
  params: { max: 1000 },
9
9
  });
@@ -1,10 +1,13 @@
1
1
  import { QueryClient, UseQueryResult } from '@tanstack/react-query';
2
2
  import { AxiosInstance } from 'axios';
3
3
  import { UserFormFields } from '../../components/users/UserForm';
4
- import { Group } from '../../types/groups';
5
- import { KeycloakQueryParams, User, UserByIdResult, UseUsersByIdsOptions } from '../../types/users';
6
- export declare const getUsers: (apiClient: AxiosInstance, username?: string, group?: string, offset?: number, limit?: number, params?: KeycloakQueryParams) => Promise<User[]>;
7
- export declare const useUsers: (apiClient: AxiosInstance, group?: string, offset?: number, limit?: number, params?: KeycloakQueryParams) => UseQueryResult<User[] | undefined, Error>;
4
+ import { Group, GroupFilter } from '../../types/groups';
5
+ import { KeycloakQueryParams, User, UserByIdResult, UsersInGroupsResult, UseUsersByIdsOptions } from '../../types/users';
6
+ export declare const getUsers: (apiClient: AxiosInstance, username?: string, group?: string | GroupFilter, offset?: number, limit?: number, params?: KeycloakQueryParams) => Promise<User[]>;
7
+ export declare const useUsers: (apiClient: AxiosInstance, group?: string | GroupFilter, offset?: number, limit?: number, params?: KeycloakQueryParams) => UseQueryResult<User[] | undefined, Error>;
8
+ export declare const useUsersInGroups: (apiClient: AxiosInstance, groups: (string | GroupFilter)[], params?: KeycloakQueryParams) => UsersInGroupsResult;
9
+ export declare const getGroupsByUser: (apiClient: AxiosInstance) => Promise<Record<string, Group[]>>;
10
+ export declare const useGroupsByUser: (apiClient: AxiosInstance, enabled?: boolean) => UseQueryResult<Record<string, Group[]> | undefined, Error>;
8
11
  export declare const getUser: (id: string, apiClient: AxiosInstance) => Promise<User>;
9
12
  export declare const useUser: (id: string, apiClient: AxiosInstance) => UseQueryResult<User | undefined, Error>;
10
13
  export declare const useUsersByIds: (apiClient: AxiosInstance, ids: (string | undefined)[], options?: UseUsersByIdsOptions) => UserByIdResult[];
@@ -38,6 +41,6 @@ export declare const useRemoveUserFromGroup: (queryClient: QueryClient, apiClien
38
41
  export declare const useSendUpdatePasswordEmail: (queryClient: QueryClient, apiClient: AxiosInstance) => import("@tanstack/react-query").UseMutationResult<{
39
42
  userId: string;
40
43
  }, Error, string, unknown>;
41
- export declare const getUsersCount: (apiClient: AxiosInstance, username?: string, group?: string) => Promise<number>;
42
- export declare const useUsersCount: (apiClient: AxiosInstance, group?: string) => UseQueryResult<number | undefined, Error>;
44
+ export declare const getUsersCount: (apiClient: AxiosInstance, username?: string, group?: string | GroupFilter) => Promise<number>;
45
+ export declare const useUsersCount: (apiClient: AxiosInstance, group?: string | GroupFilter) => UseQueryResult<number | undefined, Error>;
43
46
  //# sourceMappingURL=users.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../../src/utils/hooks/users.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EAKX,cAAc,EACf,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAGtC,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAEjE,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EACL,mBAAmB,EACnB,IAAI,EACJ,cAAc,EACd,oBAAoB,EACrB,MAAM,mBAAmB,CAAC;AAO3B,eAAO,MAAM,QAAQ,cACR,aAAa,aACb,MAAM,UACT,MAAM,WACN,MAAM,UACP,MAAM,WACJ,mBAAmB,oBA2B7B,CAAC;AAEF,eAAO,MAAM,QAAQ,cACR,aAAa,UAChB,MAAM,WACN,MAAM,UACP,MAAM,WACJ,mBAAmB,KAC3B,cAAc,CAAC,IAAI,EAAE,GAAG,SAAS,EAAE,KAAK,CAc1C,CAAC;AAGF,eAAO,MAAM,OAAO,OAAc,MAAM,aAAa,aAAa,kBAGjE,CAAC;AAEF,eAAO,MAAM,OAAO,OACd,MAAM,aACC,aAAa,KACvB,cAAc,CAAC,IAAI,GAAG,SAAS,EAAE,KAAK,CAMxC,CAAC;AAoBF,eAAO,MAAM,aAAa,cACb,aAAa,OACnB,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,YACjB,oBAAoB,KAC7B,cAAc,EAUb,CAAC;AAGL,eAAO,MAAM,OAAO,yDAKjB;IACD,OAAO,EAAE,IAAI,CAAC;IACd,mBAAmB,EAAE,OAAO,CAAC;IAC7B,SAAS,EAAE,aAAa,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB,kBAWA,CAAC;AAEF,eAAO,MAAM,UAAU,gBACR,WAAW,aACb,aAAa;UAQd,cAAc;YACZ,MAAM,EAAE,GAAG,SAAS;WA6BjC,CAAC;AAiBF,eAAO,MAAM,aAAa,gBACX,WAAW,aACb,aAAa,kFA6BzB,CAAC;AAgBF,eAAO,MAAM,aAAa,gBACX,WAAW,aACb,aAAa,4GA6BzB,CAAC;AAcF,eAAO,MAAM,aAAa,OACpB,MAAM,GAAG,SAAS,aACX,aAAa,KACvB,cAAc,CAAC,KAAK,EAAE,GAAG,SAAS,EAAE,KAAK,CAM3C,CAAC;AAmBF,eAAO,MAAM,iBAAiB,gBACf,WAAW,aACb,aAAa;;;;YAIsB,MAAM;aAAW,MAAM;WA8BtE,CAAC;AAmBF,eAAO,MAAM,sBAAsB,gBACpB,WAAW,aACb,aAAa;;;;YAIsB,MAAM;aAAW,MAAM;WA8BtE,CAAC;AAkBF,eAAO,MAAM,0BAA0B,gBACxB,WAAW,aACb,aAAa;;0BAoBzB,CAAC;AAEF,eAAO,MAAM,aAAa,cACb,aAAa,aACb,MAAM,UACT,MAAM,oBAmBf,CAAC;AAEF,eAAO,MAAM,aAAa,cACb,aAAa,UAChB,MAAM,KACb,cAAc,CAAC,MAAM,GAAG,SAAS,EAAE,KAAK,CAM1C,CAAC"}
1
+ {"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../../src/utils/hooks/users.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EAKX,cAAc,EACf,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAGtC,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAEjE,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EACL,mBAAmB,EACnB,IAAI,EACJ,cAAc,EACd,mBAAmB,EACnB,oBAAoB,EACrB,MAAM,mBAAmB,CAAC;AA8C3B,eAAO,MAAM,QAAQ,cACR,aAAa,aACb,MAAM,UACT,MAAM,GAAG,WAAW,WACpB,MAAM,UACP,MAAM,WACJ,mBAAmB,oBAkB7B,CAAC;AAkBF,eAAO,MAAM,QAAQ,cACR,aAAa,UAChB,MAAM,GAAG,WAAW,WACpB,MAAM,UACP,MAAM,WACJ,mBAAmB,KAC3B,cAAc,CAAC,IAAI,EAAE,GAAG,SAAS,EAAE,KAAK,CAM1C,CAAC;AAkBF,eAAO,MAAM,gBAAgB,cAChB,aAAa,UAChB,CAAC,MAAM,GAAG,WAAW,CAAC,EAAE,WACvB,mBAAmB,KAC3B,mBAQC,CAAC;AAOL,eAAO,MAAM,eAAe,cAAqB,aAAa,qCAc7D,CAAC;AAEF,eAAO,MAAM,eAAe,cACf,aAAa,YACf,OAAO,KACf,cAAc,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,SAAS,EAAE,KAAK,CAO3D,CAAC;AAGF,eAAO,MAAM,OAAO,OAAc,MAAM,aAAa,aAAa,kBAGjE,CAAC;AAEF,eAAO,MAAM,OAAO,OACd,MAAM,aACC,aAAa,KACvB,cAAc,CAAC,IAAI,GAAG,SAAS,EAAE,KAAK,CAMxC,CAAC;AAoBF,eAAO,MAAM,aAAa,cACb,aAAa,OACnB,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,YACjB,oBAAoB,KAC7B,cAAc,EAUb,CAAC;AAGL,eAAO,MAAM,OAAO,yDAKjB;IACD,OAAO,EAAE,IAAI,CAAC;IACd,mBAAmB,EAAE,OAAO,CAAC;IAC7B,SAAS,EAAE,aAAa,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB,kBAWA,CAAC;AAEF,eAAO,MAAM,UAAU,gBACR,WAAW,aACb,aAAa;UAQd,cAAc;YACZ,MAAM,EAAE,GAAG,SAAS;WA6BjC,CAAC;AAiBF,eAAO,MAAM,aAAa,gBACX,WAAW,aACb,aAAa,kFA6BzB,CAAC;AAgBF,eAAO,MAAM,aAAa,gBACX,WAAW,aACb,aAAa,4GA6BzB,CAAC;AAcF,eAAO,MAAM,aAAa,OACpB,MAAM,GAAG,SAAS,aACX,aAAa,KACvB,cAAc,CAAC,KAAK,EAAE,GAAG,SAAS,EAAE,KAAK,CAM3C,CAAC;AAmBF,eAAO,MAAM,iBAAiB,gBACf,WAAW,aACb,aAAa;;;;YAIsB,MAAM;aAAW,MAAM;WAmCtE,CAAC;AAmBF,eAAO,MAAM,sBAAsB,gBACpB,WAAW,aACb,aAAa;;;;YAIsB,MAAM;aAAW,MAAM;WAmCtE,CAAC;AAkBF,eAAO,MAAM,0BAA0B,gBACxB,WAAW,aACb,aAAa;;0BAoBzB,CAAC;AAEF,eAAO,MAAM,aAAa,cACb,aAAa,aACb,MAAM,UACT,MAAM,GAAG,WAAW,oBAiB7B,CAAC;AAEF,eAAO,MAAM,aAAa,cACb,aAAa,UAChB,MAAM,GAAG,WAAW,KAC3B,cAAc,CAAC,MAAM,GAAG,SAAS,EAAE,KAAK,CAS1C,CAAC"}
@@ -1,46 +1,110 @@
1
1
  import { useMutation, useQueries, useQuery, } from '@tanstack/react-query';
2
2
  import { enqueueSnackbar } from 'notistack';
3
3
  import usersTranslations from '../../lang/users';
4
+ import { getGroups } from './groups';
4
5
  import useTranslate from './useTranslate';
5
6
  const isServiceAccount = (u) => !!u.serviceAccountClientId || !!u.username?.startsWith('service-account-');
7
+ const getExtraParams = (params) => params?.briefRepresentation !== undefined
8
+ ? { briefRepresentation: params.briefRepresentation }
9
+ : {};
10
+ // Returns the id of the group, looking it up by name only when it is missing
11
+ const getGroupId = async (apiClient, group) => {
12
+ const { name, id } = typeof group === 'string' ? { name: group } : group;
13
+ if (id)
14
+ return id;
15
+ const { data: groupData } = await apiClient.get(`/groups`, {
16
+ params: { max: 1, search: name, exact: 'true' },
17
+ });
18
+ return groupData.length === 1 ? groupData[0].id : undefined;
19
+ };
20
+ // Value identifying the group in the query keys: the id when known, so that
21
+ // the same group passed by name or by id does not change the key format
22
+ const getGroupKey = (group) => typeof group === 'string' ? group : (group?.id ?? group?.name);
23
+ // Get the members of a group by id, excluding service accounts
24
+ const getGroupMembers = async (apiClient, groupId, offset = 0, limit = 1000, params) => {
25
+ const { data } = await apiClient.get(`/groups/${groupId}/members`, {
26
+ params: { max: limit, first: offset, ...getExtraParams(params) },
27
+ });
28
+ return data.filter((u) => !isServiceAccount(u));
29
+ };
6
30
  // Get list of users
7
31
  export const getUsers = async (apiClient, username, group, offset = 0, limit = 1000, params) => {
8
- const { briefRepresentation } = params ?? {};
9
- const extraParams = briefRepresentation !== undefined ? { briefRepresentation } : {};
10
32
  if (group) {
11
- const { data: groupData } = await apiClient.get(`/groups`, {
12
- params: { max: 1, search: group, exact: 'true' },
13
- });
14
- if (groupData.length !== 1)
33
+ const groupId = await getGroupId(apiClient, group);
34
+ if (!groupId)
15
35
  return [];
16
- const { data } = await apiClient.get(`/groups/${groupData[0].id}/members`, { params: { max: limit, first: offset, ...extraParams } });
17
- return data.filter((u) => !isServiceAccount(u));
36
+ return getGroupMembers(apiClient, groupId, offset, limit, params);
18
37
  }
19
38
  const { data } = await apiClient.get(`/users`, {
20
39
  params: {
21
40
  max: limit,
22
41
  first: offset,
23
42
  ...(username ? { username, exact: 'true' } : {}),
24
- ...extraParams,
43
+ ...getExtraParams(params),
25
44
  },
26
45
  });
27
46
  return data;
28
47
  };
48
+ // Shared by useUsers and useUsersInGroups, so they use the same cache
49
+ const getUsersQueryKey = (group, offset, limit, params) => [
50
+ 'users',
51
+ `users-${JSON.stringify(getGroupKey(group))}`,
52
+ `${JSON.stringify(offset)}`,
53
+ `${JSON.stringify(limit)}`,
54
+ ...(params?.briefRepresentation !== undefined
55
+ ? [`brief-${params.briefRepresentation}`]
56
+ : []),
57
+ ];
29
58
  export const useUsers = (apiClient, group, offset = 0, limit = 1000, params) => {
30
59
  return useQuery({
31
- queryKey: [
32
- 'users',
33
- `users-${JSON.stringify(group)}`,
34
- `${JSON.stringify(offset)}`,
35
- `${JSON.stringify(limit)}`,
36
- ...(params?.briefRepresentation !== undefined
37
- ? [`brief-${params.briefRepresentation}`]
38
- : []),
39
- ],
60
+ queryKey: getUsersQueryKey(group, offset, limit, params),
40
61
  queryFn: () => getUsers(apiClient, undefined, group, offset, limit, params),
41
62
  staleTime: 0,
42
63
  });
43
64
  };
65
+ // Defined OUTSIDE the hook to keep its identity stable (see combineUsers)
66
+ const combineGroupsUsers = (results) => {
67
+ const isLoading = results.some((r) => r.isLoading);
68
+ const isError = results.some((r) => r.isError);
69
+ if (results.some((r) => !r.data))
70
+ return { isLoading, isError };
71
+ // A user in more than one of the groups is returned once
72
+ const usersById = new Map();
73
+ results.forEach((r) => r.data?.forEach((u) => usersById.set(u.id, u)));
74
+ return { data: Array.from(usersById.values()), isLoading, isError };
75
+ };
76
+ // Users that are members of at least one of the groups, without duplicates.
77
+ // One request per group, sharing the cache with useUsers.
78
+ export const useUsersInGroups = (apiClient, groups, params) => useQueries({
79
+ queries: groups.map((group) => ({
80
+ queryKey: getUsersQueryKey(group, 0, 1000, params),
81
+ queryFn: () => getUsers(apiClient, undefined, group, 0, 1000, params),
82
+ staleTime: 0,
83
+ })),
84
+ combine: combineGroupsUsers,
85
+ });
86
+ // Get the groups of every user, as a map from user id to groups.
87
+ // Keycloak does not return the groups in the users list, so the members of
88
+ // every group are loaded (one request per group). Only top-level groups are
89
+ // checked; users that are in no group (or only in a subgroup) are not in
90
+ // the map.
91
+ export const getGroupsByUser = async (apiClient) => {
92
+ const groups = await getGroups(apiClient);
93
+ const members = await Promise.all(groups.map((g) => getGroupMembers(apiClient, g.id, 0, 1000, { briefRepresentation: true })));
94
+ const groupsByUser = {};
95
+ groups.forEach((group, idx) => members[idx].forEach((u) => {
96
+ groupsByUser[u.id] = [...(groupsByUser[u.id] ?? []), group];
97
+ }));
98
+ return groupsByUser;
99
+ };
100
+ export const useGroupsByUser = (apiClient, enabled = true) => {
101
+ return useQuery({
102
+ queryKey: ['users', 'groups-by-user'],
103
+ queryFn: () => getGroupsByUser(apiClient),
104
+ enabled,
105
+ staleTime: 0,
106
+ });
107
+ };
44
108
  // Get single user
45
109
  export const getUser = async (id, apiClient) => {
46
110
  const { data } = await apiClient.get(`/users/${id}`);
@@ -234,6 +298,11 @@ export const useAddUserToGroup = (queryClient, apiClient) => {
234
298
  queryKey: ['groups'],
235
299
  type: 'all',
236
300
  });
301
+ // Group filters and groups column depend on the memberships
302
+ await queryClient.refetchQueries({
303
+ queryKey: ['users'],
304
+ type: 'all',
305
+ });
237
306
  },
238
307
  });
239
308
  };
@@ -272,6 +341,11 @@ export const useRemoveUserFromGroup = (queryClient, apiClient) => {
272
341
  queryKey: ['groups'],
273
342
  type: 'all',
274
343
  });
344
+ // Group filters and groups column depend on the memberships
345
+ await queryClient.refetchQueries({
346
+ queryKey: ['users'],
347
+ type: 'all',
348
+ });
275
349
  },
276
350
  });
277
351
  };
@@ -302,11 +376,9 @@ export const useSendUpdatePasswordEmail = (queryClient, apiClient) => {
302
376
  };
303
377
  export const getUsersCount = async (apiClient, username, group) => {
304
378
  if (group) {
305
- const { data: groupData } = await apiClient.get(`/groups`, {
306
- params: { max: 1, search: group, exact: 'true' },
307
- });
308
- if (groupData.length === 1) {
309
- const { data } = await apiClient.get(`/groups/${groupData[0].id}/members`, { params: { max: -1 } });
379
+ const groupId = await getGroupId(apiClient, group);
380
+ if (groupId) {
381
+ const { data } = await apiClient.get(`/groups/${groupId}/members`, { params: { max: -1 } });
310
382
  return data.length;
311
383
  }
312
384
  else {
@@ -320,7 +392,10 @@ export const getUsersCount = async (apiClient, username, group) => {
320
392
  };
321
393
  export const useUsersCount = (apiClient, group) => {
322
394
  return useQuery({
323
- queryKey: ['users-count', `users-count-${JSON.stringify(group)}`],
395
+ queryKey: [
396
+ 'users-count',
397
+ `users-count-${JSON.stringify(getGroupKey(group))}`,
398
+ ],
324
399
  queryFn: () => getUsersCount(apiClient, undefined, group),
325
400
  staleTime: 0,
326
401
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deix/rossini-identity-management",
3
- "version": "5.2.0",
3
+ "version": "5.3.0",
4
4
  "main": "lib/index.js",
5
5
  "repository": {
6
6
  "type": "git",
@@ -44,11 +44,11 @@
44
44
  "@mdx-js/react": "^3.1.0",
45
45
  "@mui/icons-material": "^9.4.0",
46
46
  "@mui/material": "^9.4.0",
47
- "@storybook/addon-docs": "^10.6.0",
48
- "@storybook/addon-links": "^10.6.0",
49
- "@storybook/addon-onboarding": "^10.6.0",
47
+ "@storybook/addon-docs": "^10.6.5",
48
+ "@storybook/addon-links": "^10.6.5",
49
+ "@storybook/addon-onboarding": "^10.6.5",
50
50
  "@storybook/mdx2-csf": "^1.1.0",
51
- "@storybook/nextjs-vite": "^10.6.1",
51
+ "@storybook/nextjs-vite": "^10.6.5",
52
52
  "@tanstack/react-query": "^5.104.0",
53
53
  "@tanstack/react-query-devtools": "^5.104.0",
54
54
  "@types/node": "^26.6.3",
@@ -60,12 +60,13 @@
60
60
  "eslint": "^10.11.0",
61
61
  "eslint-config-prettier": "^10.1.1",
62
62
  "eslint-plugin-prettier": "^5.5.6",
63
- "eslint-plugin-storybook": "^10.6.0",
63
+ "eslint-plugin-storybook": "^10.6.5",
64
64
  "json-server": "^1.0.0-beta.15",
65
- "next": "~16.3.8",
65
+ "next": "~16.4.3",
66
66
  "react": "^19.3.0",
67
67
  "react-dom": "19.3.0",
68
68
  "remark-mdx": "^3.0.0",
69
+ "storybook": "^10.6.5",
69
70
  "ts-loader": "^9.6.2",
70
71
  "typescript": "npm:@typescript/typescript6@^6.0.2",
71
72
  "typescript-eslint": "^8.70.1",