@truedat/auth 8.14.2 → 8.14.4
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.
- package/package.json +4 -4
- package/src/messages/en.js +6 -5
- package/src/messages/es.js +8 -7
- package/src/styles/UserCard.less +582 -0
- package/src/styles/UserModals.less +826 -0
- package/src/styles/Users.less +280 -0
- package/src/users/components/AdminUserRoutes.js +7 -20
- package/src/users/components/EditUser.js +7 -34
- package/src/users/components/EditUserModal.js +150 -0
- package/src/users/components/NewUser.js +128 -17
- package/src/users/components/Password.js +11 -189
- package/src/users/components/UserAcls.js +56 -6
- package/src/users/components/UserActions.js +99 -34
- package/src/users/components/UserCard.js +168 -22
- package/src/users/components/UserCards.js +273 -35
- package/src/users/components/UserFormFields.js +206 -0
- package/src/users/components/UserGroupAclRow.js +8 -2
- package/src/users/components/UserGroupAcls.js +2 -2
- package/src/users/components/UserGroupLink.js +21 -0
- package/src/users/components/UserInfoModal.js +268 -0
- package/src/users/components/UserPasswordField.js +66 -0
- package/src/users/components/UserPasswordModal.js +210 -0
- package/src/users/components/UserRoutes.js +4 -13
- package/src/users/components/UserTabs.js +5 -2
- package/src/users/components/UserTypeSelector.js +109 -0
- package/src/users/components/UsersAndGroups.js +39 -5
- package/src/users/components/UsersTable.js +234 -0
- package/src/users/components/__tests__/EditUser.spec.js +84 -10
- package/src/users/components/__tests__/NewUser.spec.js +34 -4
- package/src/users/components/__tests__/Password.spec.js +53 -146
- package/src/users/components/__tests__/UserAcls.spec.js +23 -2
- package/src/users/components/__tests__/UserCard.spec.js +111 -5
- package/src/users/components/__tests__/UserCards.spec.js +194 -0
- package/src/users/components/__tests__/UserGroupAclRow.spec.js +19 -1
- package/src/users/components/__tests__/UserModals.spec.js +507 -0
- package/src/users/components/__tests__/UserRoutes.spec.js +8 -3
- package/src/users/components/__tests__/UserTypeSelector.spec.js +32 -0
- package/src/users/components/__tests__/UsersAndGroups.spec.js +17 -0
- package/src/users/components/__tests__/__snapshots__/UserCard.spec.js.snap +64 -37
- package/src/users/components/__tests__/__snapshots__/UserCards.spec.js.snap +424 -75
- package/src/users/components/__tests__/__snapshots__/UserGroupAclRow.spec.js.snap +7 -3
- package/src/users/components/__tests__/__snapshots__/UserRoutes.spec.js.snap +4 -13
- package/src/users/hooks/useSortedGroups.js +13 -0
- package/src/users/hooks/useUserModalOperation.js +55 -0
- package/src/users/reducers/__tests__/userRedirect.spec.js +14 -3
- package/src/users/reducers/index.js +1 -0
- package/src/users/reducers/userModalOperation.js +29 -0
- package/src/users/reducers/userRedirect.js +5 -4
- package/src/users/sagas/__tests__/updateUser.spec.js +19 -1
- package/src/users/sagas/createUser.js +12 -5
- package/src/users/sagas/deleteUser.js +12 -5
- package/src/users/sagas/updateUser.js +19 -8
- package/src/users/selectors/__tests__/filterUsers.spec.js +11 -0
- package/src/users/selectors/__tests__/getUserAcls.spec.js +1 -0
- package/src/users/selectors/__tests__/getUserGroupAcls.spec.js +2 -0
- package/src/users/selectors/filterUsers.js +9 -1
- package/src/users/selectors/getUserGroupAcls.js +1 -0
- package/src/users/components/User.js +0 -68
- package/src/users/components/UserPassword.js +0 -24
- package/src/users/components/__tests__/User.spec.js +0 -20
- package/src/users/components/__tests__/UserPassword.spec.js +0 -24
- package/src/users/components/__tests__/__snapshots__/EditUser.spec.js.snap +0 -281
- package/src/users/components/__tests__/__snapshots__/NewUser.spec.js.snap +0 -321
- package/src/users/components/__tests__/__snapshots__/Password.spec.js.snap +0 -128
- package/src/users/components/__tests__/__snapshots__/User.spec.js.snap +0 -129
- package/src/users/components/__tests__/__snapshots__/UserPassword.spec.js.snap +0 -124
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import PropTypes from "prop-types";
|
|
2
|
+
import { Icon } from "semantic-ui-react";
|
|
3
|
+
import { useIntl } from "react-intl";
|
|
4
|
+
|
|
5
|
+
export const userTypeOptions = [
|
|
6
|
+
{ value: "admin", messageId: "user.type.admin", icon: "shield" },
|
|
7
|
+
{ value: "user", messageId: "user.type.user", icon: "user" },
|
|
8
|
+
{ value: "service", messageId: "user.type.service", icon: "settings" },
|
|
9
|
+
{ value: "agent", messageId: "user.type.agent", icon: "send" },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const UserTypeSelector = ({
|
|
13
|
+
id,
|
|
14
|
+
label,
|
|
15
|
+
value,
|
|
16
|
+
onChange,
|
|
17
|
+
onBlur,
|
|
18
|
+
required,
|
|
19
|
+
disabled,
|
|
20
|
+
}) => {
|
|
21
|
+
const { formatMessage } = useIntl();
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className={`field ${required ? "required" : ""}`}>
|
|
25
|
+
<label id={`${id}-label`} htmlFor={id}>
|
|
26
|
+
{label}
|
|
27
|
+
</label>
|
|
28
|
+
<div
|
|
29
|
+
id={id}
|
|
30
|
+
className="um-user-type-selector"
|
|
31
|
+
role="radiogroup"
|
|
32
|
+
aria-labelledby={`${id}-label`}
|
|
33
|
+
>
|
|
34
|
+
{userTypeOptions.map(({ value: typeValue, messageId, icon }) => {
|
|
35
|
+
const isSelected = value === typeValue;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<button
|
|
39
|
+
key={typeValue}
|
|
40
|
+
type="button"
|
|
41
|
+
disabled={disabled}
|
|
42
|
+
tabIndex={isSelected ? 0 : -1}
|
|
43
|
+
onKeyDown={(event) => {
|
|
44
|
+
const keys = [
|
|
45
|
+
"ArrowRight",
|
|
46
|
+
"ArrowDown",
|
|
47
|
+
"ArrowLeft",
|
|
48
|
+
"ArrowUp",
|
|
49
|
+
"Home",
|
|
50
|
+
"End",
|
|
51
|
+
];
|
|
52
|
+
if (!keys.includes(event.key)) return;
|
|
53
|
+
event.preventDefault();
|
|
54
|
+
const index = userTypeOptions.findIndex(
|
|
55
|
+
(option) => option.value === typeValue,
|
|
56
|
+
);
|
|
57
|
+
const next =
|
|
58
|
+
event.key === "Home"
|
|
59
|
+
? 0
|
|
60
|
+
: event.key === "End"
|
|
61
|
+
? userTypeOptions.length - 1
|
|
62
|
+
: (index +
|
|
63
|
+
(keys.indexOf(event.key) < 2 ? 1 : -1) +
|
|
64
|
+
userTypeOptions.length) %
|
|
65
|
+
userTypeOptions.length;
|
|
66
|
+
onChange(userTypeOptions[next].value);
|
|
67
|
+
event.currentTarget.parentElement
|
|
68
|
+
.querySelectorAll("button")
|
|
69
|
+
[next].focus();
|
|
70
|
+
}}
|
|
71
|
+
className={`um-user-type-button ${
|
|
72
|
+
isSelected ? "um-user-type-button--active" : ""
|
|
73
|
+
}`}
|
|
74
|
+
role="radio"
|
|
75
|
+
aria-checked={isSelected}
|
|
76
|
+
aria-label={formatMessage({ id: messageId })}
|
|
77
|
+
onClick={() => onChange(typeValue)}
|
|
78
|
+
onBlur={onBlur}
|
|
79
|
+
>
|
|
80
|
+
<Icon name={icon} />
|
|
81
|
+
<span>{formatMessage({ id: messageId })}</span>
|
|
82
|
+
</button>
|
|
83
|
+
);
|
|
84
|
+
})}
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
UserTypeSelector.propTypes = {
|
|
91
|
+
id: PropTypes.string,
|
|
92
|
+
label: PropTypes.string,
|
|
93
|
+
onBlur: PropTypes.func,
|
|
94
|
+
onChange: PropTypes.func,
|
|
95
|
+
required: PropTypes.bool,
|
|
96
|
+
disabled: PropTypes.bool,
|
|
97
|
+
value: PropTypes.string,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
UserTypeSelector.defaultProps = {
|
|
101
|
+
id: "role",
|
|
102
|
+
label: "",
|
|
103
|
+
onBlur: () => {},
|
|
104
|
+
onChange: () => {},
|
|
105
|
+
required: false,
|
|
106
|
+
value: "user",
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export default UserTypeSelector;
|
|
@@ -1,15 +1,37 @@
|
|
|
1
|
-
import { Route, Routes } from "react-router";
|
|
2
1
|
import { Header, Icon, Segment } from "semantic-ui-react";
|
|
3
2
|
import { FormattedMessage, useIntl } from "react-intl";
|
|
4
|
-
import {
|
|
3
|
+
import { useLocation, useNavigate, useParams } from "react-router";
|
|
4
|
+
import { Loader } from "@truedat/core/router";
|
|
5
5
|
import { GROUPS, USERS } from "@truedat/core/routes";
|
|
6
6
|
import { GroupsList } from "../../groups/components/GroupCards";
|
|
7
|
+
import UserLoader from "./UserLoader";
|
|
7
8
|
import UserCards from "./UserCards";
|
|
8
9
|
import UserTabs from "./UserTabs";
|
|
10
|
+
import UserInfoModal from "./UserInfoModal";
|
|
11
|
+
import EditUserModal from "./EditUserModal";
|
|
12
|
+
import UserPasswordModal from "./UserPasswordModal";
|
|
13
|
+
import NewUser from "./NewUser";
|
|
14
|
+
|
|
15
|
+
const getModalType = (pathname, id) => {
|
|
16
|
+
if (pathname.endsWith("/new")) return "new";
|
|
17
|
+
if (!id) return null;
|
|
18
|
+
if (pathname.endsWith("/edit")) return "edit";
|
|
19
|
+
if (pathname.endsWith("/password")) return "password";
|
|
20
|
+
return "info";
|
|
21
|
+
};
|
|
9
22
|
|
|
10
23
|
export const UsersAndGroups = () => {
|
|
11
24
|
const { formatMessage } = useIntl();
|
|
12
|
-
const
|
|
25
|
+
const { pathname } = useLocation();
|
|
26
|
+
const navigate = useNavigate();
|
|
27
|
+
const { id } = useParams();
|
|
28
|
+
const modalType = getModalType(pathname, id);
|
|
29
|
+
const isUsersPath = pathname === USERS || pathname.startsWith(USERS + "/");
|
|
30
|
+
const isGroupsPath = pathname === GROUPS || pathname.startsWith(GROUPS + "/");
|
|
31
|
+
const onClose = () => navigate(USERS);
|
|
32
|
+
|
|
33
|
+
const isNewModal = modalType === "new";
|
|
34
|
+
const isUserModal = Boolean(isUsersPath && (id || isNewModal));
|
|
13
35
|
|
|
14
36
|
return (
|
|
15
37
|
<Segment>
|
|
@@ -30,9 +52,21 @@ export const UsersAndGroups = () => {
|
|
|
30
52
|
</Header>
|
|
31
53
|
<UserTabs />
|
|
32
54
|
<Segment attached="bottom">
|
|
33
|
-
{
|
|
34
|
-
{
|
|
55
|
+
{isUsersPath && <UserCards />}
|
|
56
|
+
{isGroupsPath && <GroupsList />}
|
|
35
57
|
</Segment>
|
|
58
|
+
|
|
59
|
+
{id && isUserModal && <Loader loaders={<UserLoader />} />}
|
|
60
|
+
{isUserModal && modalType === "new" && <NewUser onClose={onClose} />}
|
|
61
|
+
{isUserModal && modalType === "info" && (
|
|
62
|
+
<UserInfoModal onClose={onClose} />
|
|
63
|
+
)}
|
|
64
|
+
{isUserModal && modalType === "edit" && (
|
|
65
|
+
<EditUserModal onClose={onClose} />
|
|
66
|
+
)}
|
|
67
|
+
{isUserModal && modalType === "password" && (
|
|
68
|
+
<UserPasswordModal onClose={onClose} />
|
|
69
|
+
)}
|
|
36
70
|
</Segment>
|
|
37
71
|
);
|
|
38
72
|
};
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import PropTypes from "prop-types";
|
|
3
|
+
import { Link } from "react-router";
|
|
4
|
+
import { useIntl } from "react-intl";
|
|
5
|
+
import { Button, Icon, Table } from "semantic-ui-react";
|
|
6
|
+
import { linkTo } from "@truedat/core/routes";
|
|
7
|
+
import { lowerDeburrTrim } from "@truedat/core/services/sort";
|
|
8
|
+
import { DeleteUserModal } from "./UserActions";
|
|
9
|
+
import { userTypeOptions } from "./UserTypeSelector";
|
|
10
|
+
|
|
11
|
+
const columns = [
|
|
12
|
+
"type",
|
|
13
|
+
"name",
|
|
14
|
+
"external_id",
|
|
15
|
+
"email",
|
|
16
|
+
"groups",
|
|
17
|
+
"roles",
|
|
18
|
+
"actions",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export default function UsersTable({ users, sort, onSort }) {
|
|
22
|
+
const { formatMessage } = useIntl();
|
|
23
|
+
const [deleting, setDeleting] = useState(null);
|
|
24
|
+
const message = (key) => formatMessage({ id: `users.table.${key}` });
|
|
25
|
+
const sortValue = (user) => {
|
|
26
|
+
if (sort.column === "groups") return user.groups?.length || 0;
|
|
27
|
+
if (sort.column === "type")
|
|
28
|
+
return lowerDeburrTrim(
|
|
29
|
+
formatMessage({
|
|
30
|
+
id: `user.type.${user.role}`,
|
|
31
|
+
defaultMessage: user.role || "",
|
|
32
|
+
}),
|
|
33
|
+
);
|
|
34
|
+
return lowerDeburrTrim(
|
|
35
|
+
sort.column === "name"
|
|
36
|
+
? user.full_name?.trim() || user.user_name
|
|
37
|
+
: user[sort.column],
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
const sorted = [...users].sort((a, b) => {
|
|
41
|
+
const left = sortValue(a);
|
|
42
|
+
const right = sortValue(b);
|
|
43
|
+
const order =
|
|
44
|
+
typeof left === "number" ? left - right : left.localeCompare(right);
|
|
45
|
+
return (
|
|
46
|
+
(sort.ascending ? order : -order) ||
|
|
47
|
+
String(a.id).localeCompare(String(b.id))
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
return (
|
|
51
|
+
<>
|
|
52
|
+
<div
|
|
53
|
+
className="users-table-scroll"
|
|
54
|
+
role="region"
|
|
55
|
+
aria-label={message("view")}
|
|
56
|
+
tabIndex={0}
|
|
57
|
+
>
|
|
58
|
+
<Table unstackable className="users-table">
|
|
59
|
+
<Table.Header>
|
|
60
|
+
<Table.Row>
|
|
61
|
+
{columns.map((column) => (
|
|
62
|
+
<Table.HeaderCell
|
|
63
|
+
key={column}
|
|
64
|
+
scope="col"
|
|
65
|
+
textAlign={
|
|
66
|
+
column === "actions"
|
|
67
|
+
? "right"
|
|
68
|
+
: column === "type"
|
|
69
|
+
? "center"
|
|
70
|
+
: undefined
|
|
71
|
+
}
|
|
72
|
+
aria-sort={
|
|
73
|
+
sort.column === column
|
|
74
|
+
? sort.ascending
|
|
75
|
+
? "ascending"
|
|
76
|
+
: "descending"
|
|
77
|
+
: undefined
|
|
78
|
+
}
|
|
79
|
+
>
|
|
80
|
+
{["roles", "actions"].includes(column) ? (
|
|
81
|
+
message(column)
|
|
82
|
+
) : (
|
|
83
|
+
<button
|
|
84
|
+
type="button"
|
|
85
|
+
className="users-table-sort"
|
|
86
|
+
onClick={() =>
|
|
87
|
+
onSort({
|
|
88
|
+
column,
|
|
89
|
+
ascending:
|
|
90
|
+
sort.column === column ? !sort.ascending : true,
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
>
|
|
94
|
+
{message(column)}
|
|
95
|
+
<Icon
|
|
96
|
+
className={
|
|
97
|
+
sort.column === column ? "" : "users-sort-hidden"
|
|
98
|
+
}
|
|
99
|
+
name={sort.ascending ? "angle up" : "angle down"}
|
|
100
|
+
aria-hidden="true"
|
|
101
|
+
/>
|
|
102
|
+
</button>
|
|
103
|
+
)}
|
|
104
|
+
</Table.HeaderCell>
|
|
105
|
+
))}
|
|
106
|
+
</Table.Row>
|
|
107
|
+
</Table.Header>
|
|
108
|
+
<Table.Body>
|
|
109
|
+
{sorted.map((user) => {
|
|
110
|
+
const name = user.full_name?.trim() || user.user_name;
|
|
111
|
+
const type = userTypeOptions.find(
|
|
112
|
+
(option) => option.value === user.role,
|
|
113
|
+
);
|
|
114
|
+
const typeLabel = formatMessage({
|
|
115
|
+
id: `user.type.${user.role}`,
|
|
116
|
+
defaultMessage: user.role || message("unknown"),
|
|
117
|
+
});
|
|
118
|
+
const roles =
|
|
119
|
+
Array.isArray(user.acls) &&
|
|
120
|
+
user.acls.every(
|
|
121
|
+
(acl) => acl?.role?.id != null || acl?.role?.name,
|
|
122
|
+
)
|
|
123
|
+
? new Set(
|
|
124
|
+
user.acls.map(({ role }) =>
|
|
125
|
+
role.id != null ? `id:${role.id}` : `name:${role.name}`,
|
|
126
|
+
),
|
|
127
|
+
).size
|
|
128
|
+
: null;
|
|
129
|
+
return (
|
|
130
|
+
<Table.Row key={user.id}>
|
|
131
|
+
<Table.Cell
|
|
132
|
+
textAlign="center"
|
|
133
|
+
verticalAlign="middle"
|
|
134
|
+
className="users-table-type"
|
|
135
|
+
>
|
|
136
|
+
<span title={typeLabel} role="img" aria-label={typeLabel}>
|
|
137
|
+
<Icon
|
|
138
|
+
name={type?.icon || "question circle"}
|
|
139
|
+
aria-hidden="true"
|
|
140
|
+
/>
|
|
141
|
+
</span>
|
|
142
|
+
</Table.Cell>
|
|
143
|
+
<Table.Cell>
|
|
144
|
+
<Link
|
|
145
|
+
className="users-table-name"
|
|
146
|
+
to={linkTo.USER({ id: user.id })}
|
|
147
|
+
>
|
|
148
|
+
{name}
|
|
149
|
+
</Link>
|
|
150
|
+
{user.full_name?.trim() && user.user_name && (
|
|
151
|
+
<span className="users-table-username">
|
|
152
|
+
{user.user_name}
|
|
153
|
+
</span>
|
|
154
|
+
)}
|
|
155
|
+
</Table.Cell>
|
|
156
|
+
<Table.Cell>{user.external_id || "—"}</Table.Cell>
|
|
157
|
+
<Table.Cell>{user.email || "—"}</Table.Cell>
|
|
158
|
+
<Table.Cell>
|
|
159
|
+
{user.groups?.length ? (
|
|
160
|
+
<span
|
|
161
|
+
aria-label={`${message("groups")}: ${user.groups.length}`}
|
|
162
|
+
>
|
|
163
|
+
<Icon name="users" aria-hidden="true" />
|
|
164
|
+
{user.groups.length}
|
|
165
|
+
</span>
|
|
166
|
+
) : (
|
|
167
|
+
"—"
|
|
168
|
+
)}
|
|
169
|
+
</Table.Cell>
|
|
170
|
+
<Table.Cell>
|
|
171
|
+
{roles === null ? (
|
|
172
|
+
message("unknown")
|
|
173
|
+
) : roles ? (
|
|
174
|
+
<span aria-label={`${message("roles")}: ${roles}`}>
|
|
175
|
+
<Icon name="shield" aria-hidden="true" />
|
|
176
|
+
{roles}
|
|
177
|
+
</span>
|
|
178
|
+
) : (
|
|
179
|
+
"—"
|
|
180
|
+
)}
|
|
181
|
+
</Table.Cell>
|
|
182
|
+
<Table.Cell textAlign="right" className="users-table-actions">
|
|
183
|
+
<Button
|
|
184
|
+
as={Link}
|
|
185
|
+
role="link"
|
|
186
|
+
to={linkTo.USER({ id: user.id })}
|
|
187
|
+
icon="eye"
|
|
188
|
+
basic
|
|
189
|
+
aria-label={message("show")}
|
|
190
|
+
title={message("show")}
|
|
191
|
+
/>
|
|
192
|
+
<Button
|
|
193
|
+
as={Link}
|
|
194
|
+
role="link"
|
|
195
|
+
to={linkTo.USER_EDIT({ id: user.id })}
|
|
196
|
+
icon="pencil"
|
|
197
|
+
basic
|
|
198
|
+
aria-label={message("edit")}
|
|
199
|
+
title={message("edit")}
|
|
200
|
+
/>
|
|
201
|
+
<Button
|
|
202
|
+
icon="trash alternate outline"
|
|
203
|
+
basic
|
|
204
|
+
color="red"
|
|
205
|
+
aria-label={message("delete")}
|
|
206
|
+
title={message("delete")}
|
|
207
|
+
onClick={() => setDeleting(user)}
|
|
208
|
+
/>
|
|
209
|
+
</Table.Cell>
|
|
210
|
+
</Table.Row>
|
|
211
|
+
);
|
|
212
|
+
})}
|
|
213
|
+
</Table.Body>
|
|
214
|
+
</Table>
|
|
215
|
+
</div>
|
|
216
|
+
{deleting && (
|
|
217
|
+
<DeleteUserModal
|
|
218
|
+
id={deleting.id}
|
|
219
|
+
name={deleting.full_name || deleting.user_name}
|
|
220
|
+
onClose={() => setDeleting(null)}
|
|
221
|
+
/>
|
|
222
|
+
)}
|
|
223
|
+
</>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
UsersTable.propTypes = {
|
|
228
|
+
users: PropTypes.array.isRequired,
|
|
229
|
+
sort: PropTypes.shape({
|
|
230
|
+
column: PropTypes.string.isRequired,
|
|
231
|
+
ascending: PropTypes.bool.isRequired,
|
|
232
|
+
}).isRequired,
|
|
233
|
+
onSort: PropTypes.func.isRequired,
|
|
234
|
+
};
|
|
@@ -1,18 +1,92 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { act, fireEvent, waitFor } from "@testing-library/react";
|
|
2
|
+
import { Provider } from "react-redux";
|
|
3
|
+
import { createStore, combineReducers, applyMiddleware } from "redux";
|
|
4
|
+
import { Route, Routes } from "react-router";
|
|
5
|
+
import { render } from "@truedat/test/render";
|
|
6
|
+
import { userModalOperation } from "../../reducers/userModalOperation";
|
|
7
|
+
import { updateUser } from "../../routines";
|
|
2
8
|
import EditUser from "../EditUser";
|
|
3
9
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
10
|
+
jest.mock("../../hooks/useSortedGroups", () => ({
|
|
11
|
+
__esModule: true,
|
|
12
|
+
default: () => [],
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
const account = {
|
|
16
|
+
id: 123,
|
|
17
|
+
user_name: "test.user",
|
|
18
|
+
full_name: "Test User",
|
|
19
|
+
role: "user",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const routes = (
|
|
23
|
+
<Routes>
|
|
24
|
+
<Route path="/users/:id/edit" element={<EditUser />} />
|
|
25
|
+
<Route path="/users" element={<div>User list</div>} />
|
|
26
|
+
</Routes>
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const renderOptions = {
|
|
30
|
+
state: { user: account },
|
|
31
|
+
routes: ["/users/123/edit"],
|
|
8
32
|
};
|
|
9
33
|
|
|
10
34
|
describe("<EditUser />", () => {
|
|
11
|
-
|
|
35
|
+
it("shows the selected Redux user in the shared modal and cancels to the list", async () => {
|
|
36
|
+
const dispatch = jest.fn();
|
|
37
|
+
const rendered = render(routes, { ...renderOptions, dispatch });
|
|
38
|
+
expect(rendered.getByRole("dialog")).toHaveClass("user-modal--edit");
|
|
39
|
+
expect(document.getElementById("full_name")).toHaveValue(account.full_name);
|
|
40
|
+
fireEvent.click(rendered.getByRole("button", { name: "actions.cancel" }));
|
|
41
|
+
expect(await rendered.findByText("User list")).toBeVisible();
|
|
42
|
+
expect(dispatch).not.toHaveBeenCalledWith(
|
|
43
|
+
expect.objectContaining({ type: updateUser.TRIGGER }),
|
|
44
|
+
);
|
|
45
|
+
});
|
|
12
46
|
|
|
13
|
-
it("
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
47
|
+
it("retains errors and closes only after its own successful update", async () => {
|
|
48
|
+
const actions = [];
|
|
49
|
+
const store = createStore(
|
|
50
|
+
combineReducers({ user: () => account, userModalOperation }),
|
|
51
|
+
applyMiddleware(() => (next) => (action) => {
|
|
52
|
+
actions.push(action);
|
|
53
|
+
return next(action);
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
const rendered = render(
|
|
57
|
+
<Provider store={store}>{routes}</Provider>,
|
|
58
|
+
renderOptions,
|
|
59
|
+
);
|
|
60
|
+
fireEvent.change(document.getElementById("full_name"), {
|
|
61
|
+
target: { value: "Updated User" },
|
|
62
|
+
});
|
|
63
|
+
fireEvent.submit(document.getElementById("edit-user-form"));
|
|
64
|
+
await waitFor(() =>
|
|
65
|
+
expect(
|
|
66
|
+
actions.filter(({ type }) => type === updateUser.TRIGGER),
|
|
67
|
+
).toHaveLength(1),
|
|
68
|
+
);
|
|
69
|
+
const request = actions.find(({ type }) => type === updateUser.TRIGGER);
|
|
70
|
+
expect(request.payload).toEqual(
|
|
71
|
+
expect.objectContaining({ id: account.id, full_name: "Updated User" }),
|
|
72
|
+
);
|
|
73
|
+
act(() =>
|
|
74
|
+
store.dispatch({ ...updateUser.failure("error"), meta: request.meta }),
|
|
75
|
+
);
|
|
76
|
+
expect(rendered.getByRole("dialog")).toBeVisible();
|
|
77
|
+
expect(document.getElementById("full_name")).toHaveValue("Updated User");
|
|
78
|
+
fireEvent.submit(document.getElementById("edit-user-form"));
|
|
79
|
+
await waitFor(() =>
|
|
80
|
+
expect(
|
|
81
|
+
actions.filter(({ type }) => type === updateUser.TRIGGER),
|
|
82
|
+
).toHaveLength(2),
|
|
83
|
+
);
|
|
84
|
+
const retry = actions.filter(({ type }) => type === updateUser.TRIGGER)[1];
|
|
85
|
+
act(() =>
|
|
86
|
+
store.dispatch({ ...updateUser.success({}), meta: request.meta }),
|
|
87
|
+
);
|
|
88
|
+
expect(rendered.getByRole("dialog")).toBeVisible();
|
|
89
|
+
act(() => store.dispatch({ ...updateUser.success({}), meta: retry.meta }));
|
|
90
|
+
expect(await rendered.findByText("User list")).toBeVisible();
|
|
17
91
|
});
|
|
18
92
|
});
|
|
@@ -1,10 +1,40 @@
|
|
|
1
1
|
import { render, waitForLoad } from "@truedat/test/render";
|
|
2
|
-
import NewUser from "../NewUser";
|
|
2
|
+
import { NewUser } from "../NewUser";
|
|
3
3
|
|
|
4
4
|
describe("<NewUser />", () => {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
const props = {
|
|
6
|
+
createUser: jest.fn(),
|
|
7
|
+
groups: [{ id: 1, name: "data", alias: "Data" }],
|
|
8
|
+
isSubmitting: false,
|
|
9
|
+
onClose: jest.fn(),
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
it("renders the new user modal fields", async () => {
|
|
13
|
+
const rendered = render(<NewUser {...props} />);
|
|
14
|
+
await waitForLoad(rendered);
|
|
15
|
+
|
|
16
|
+
expect(rendered.getByText(/users.actions.create/i)).toBeInTheDocument();
|
|
17
|
+
expect(
|
|
18
|
+
rendered.getByPlaceholderText(/user.form.placeholder.full_name/i),
|
|
19
|
+
).toBeInTheDocument();
|
|
20
|
+
expect(
|
|
21
|
+
rendered.getByPlaceholderText(/user.form.placeholder.email/i),
|
|
22
|
+
).toBeInTheDocument();
|
|
23
|
+
expect(
|
|
24
|
+
rendered.getByPlaceholderText(/user.form.placeholder.user_name/i),
|
|
25
|
+
).toBeInTheDocument();
|
|
26
|
+
expect(rendered.baseElement.querySelector("#password")).toBeVisible();
|
|
27
|
+
expect(
|
|
28
|
+
rendered.getByLabelText(/user.form.password_confirmation/i),
|
|
29
|
+
).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("disables creation until the form is valid", async () => {
|
|
33
|
+
const rendered = render(<NewUser {...props} />);
|
|
7
34
|
await waitForLoad(rendered);
|
|
8
|
-
|
|
35
|
+
|
|
36
|
+
expect(
|
|
37
|
+
rendered.getByRole("button", { name: /user.create.submit/i }),
|
|
38
|
+
).toBeDisabled();
|
|
9
39
|
});
|
|
10
40
|
});
|