@truedat/auth 8.14.0 → 8.14.2
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/groups/components/EditGroup.js +64 -20
- package/src/groups/components/Group.js +39 -41
- package/src/groups/components/GroupCard.js +51 -54
- package/src/groups/components/GroupCards.js +60 -25
- package/src/groups/components/GroupDeleteModal.js +98 -0
- package/src/groups/components/GroupDropdown.js +92 -0
- package/src/groups/components/GroupForm.js +165 -84
- package/src/groups/components/GroupFormModal.js +137 -0
- package/src/groups/components/GroupInfoModal.js +114 -0
- package/src/groups/components/GroupMemberPicker.js +364 -0
- package/src/groups/components/GroupRouteModal.js +46 -0
- package/src/groups/components/GroupUsers.js +31 -14
- package/src/groups/components/GroupUsersTable.js +136 -83
- package/src/groups/components/NewGroup.js +38 -10
- package/src/groups/components/__tests__/EditGroup.spec.js +105 -0
- package/src/groups/components/__tests__/Group.spec.js +29 -47
- package/src/groups/components/__tests__/GroupCard.spec.js +8 -20
- package/src/groups/components/__tests__/GroupDropdown.spec.js +27 -0
- package/src/groups/components/__tests__/GroupForm.spec.js +32 -5
- package/src/groups/components/__tests__/GroupMemberPicker.spec.js +378 -0
- package/src/groups/components/__tests__/GroupModals.spec.js +210 -0
- package/src/groups/components/__tests__/GroupUsers.spec.js +17 -2
- package/src/groups/components/__tests__/GroupUsersTable.spec.js +34 -2
- package/src/groups/components/__tests__/__snapshots__/Group.spec.js.snap +1 -0
- package/src/groups/components/__tests__/__snapshots__/GroupCard.spec.js.snap +32 -30
- package/src/groups/components/__tests__/__snapshots__/GroupCards.spec.js.snap +90 -77
- package/src/groups/components/__tests__/__snapshots__/GroupUsers.spec.js.snap +75 -11
- package/src/groups/components/__tests__/__snapshots__/GroupUsersTable.spec.js.snap +70 -13
- package/src/groups/components/index.js +0 -4
- package/src/groups/hooks/__tests__/useGroupOperation.spec.js +167 -0
- package/src/groups/hooks/__tests__/useGroups.spec.js +90 -0
- package/src/groups/hooks/useGroupOperation.js +83 -0
- package/src/groups/hooks/useGroups.js +64 -0
- package/src/groups/reducers/index.js +0 -4
- package/src/groups/routines.js +0 -7
- package/src/groups/sagas/index.js +2 -24
- package/src/hooks/useRoles.js +6 -3
- package/src/styles/Groups.less +1009 -0
- package/src/users/components/AdminUserRoutes.js +1 -14
- package/src/users/components/GroupUserCrumbs.js +7 -12
- package/src/users/components/UserRoutes.js +12 -35
- package/src/users/components/UsersAndGroups.js +3 -6
- package/src/users/components/__tests__/GroupUserCrumbs.spec.js +26 -30
- package/src/users/components/__tests__/UserRoutes.spec.js +4 -6
- package/src/users/components/__tests__/UsersAndGroups.spec.js +5 -7
- package/src/users/components/__tests__/__snapshots__/UserRoutes.spec.js.snap +9 -55
- package/src/groups/components/GroupLoader.js +0 -27
- package/src/groups/components/GroupsLoader.js +0 -30
- package/src/groups/components/__tests__/GroupLoader.spec.js +0 -71
- package/src/groups/components/__tests__/__snapshots__/GroupLoader.spec.js.snap +0 -11
- package/src/groups/reducers/__tests__/groupRedirect.spec.js +0 -51
- package/src/groups/reducers/__tests__/groupUsers.spec.js +0 -60
- package/src/groups/reducers/group.js +0 -56
- package/src/groups/reducers/groupRedirect.js +0 -20
- package/src/groups/reducers/groupUsers.js +0 -16
- package/src/groups/reducers/groups.js +0 -29
- package/src/groups/sagas/__tests__/deleteGroup.spec.js +0 -93
- package/src/groups/sagas/__tests__/deleteGroupUser.spec.js +0 -80
- package/src/groups/sagas/createGroup.js +0 -27
- package/src/groups/sagas/deleteGroup.js +0 -31
- package/src/groups/sagas/deleteGroupUser.js +0 -35
- package/src/groups/sagas/fetchGroup.js +0 -30
- package/src/groups/sagas/fetchGroups.js +0 -26
- package/src/groups/sagas/updateGroup.js +0 -28
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
import _ from "lodash/fp";
|
|
2
|
-
import { useState } from "react";
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
3
3
|
import PropTypes from "prop-types";
|
|
4
4
|
import { useIntl } from "react-intl";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
import ConfirmModal from "@truedat/core/components/ConfirmModal";
|
|
5
|
+
import { Link } from "react-router";
|
|
6
|
+
import { Table, Icon, Button } from "semantic-ui-react";
|
|
7
|
+
import SearchInput from "@truedat/core/components/SearchInput";
|
|
9
8
|
import { linkTo } from "@truedat/core/routes";
|
|
10
9
|
import { columnDecorator } from "@truedat/core/services";
|
|
11
10
|
import { accentInsensitivePathOrder } from "@truedat/core/services/sort";
|
|
12
|
-
import
|
|
11
|
+
import GroupDeleteModal from "./GroupDeleteModal";
|
|
12
|
+
|
|
13
|
+
const EMPTY_USERS = [];
|
|
13
14
|
|
|
14
15
|
const toSearchable = _.flow(_.deburr, _.toLower, _.trim);
|
|
15
16
|
|
|
16
17
|
const matchesFilter = (filter) =>
|
|
17
18
|
_.flow(
|
|
18
|
-
_.at(["user_name", "full_name", "email"]),
|
|
19
|
+
_.at(["user_name", "full_name", "email", "external_id"]),
|
|
19
20
|
_.map(toSearchable),
|
|
20
21
|
_.some(_.includes(toSearchable(filter))),
|
|
21
22
|
);
|
|
@@ -24,15 +25,18 @@ const columns = [
|
|
|
24
25
|
{ name: "full_name", width: 2, sort: { name: "full_name" } },
|
|
25
26
|
{ name: "user_name", width: 2, sort: { name: "user_name" } },
|
|
26
27
|
{ name: "email", width: 2, sort: { name: "email" } },
|
|
28
|
+
{ name: "external_id", width: 2, sort: { name: "external_id" } },
|
|
27
29
|
];
|
|
28
30
|
|
|
29
31
|
export const GroupUsersTable = ({
|
|
30
32
|
filter,
|
|
33
|
+
onFilterChange,
|
|
31
34
|
groupUsers,
|
|
32
|
-
|
|
35
|
+
users = EMPTY_USERS,
|
|
33
36
|
group,
|
|
37
|
+
readOnly = false,
|
|
34
38
|
}) => {
|
|
35
|
-
const
|
|
39
|
+
const [removing, setRemoving] = useState(null);
|
|
36
40
|
const [sortBy, setSortBy] = useState("full_name");
|
|
37
41
|
const [ascending, setAscending] = useState(true);
|
|
38
42
|
const { formatMessage } = useIntl();
|
|
@@ -44,97 +48,146 @@ export const GroupUsersTable = ({
|
|
|
44
48
|
setAscending(true);
|
|
45
49
|
}
|
|
46
50
|
};
|
|
51
|
+
const members = useMemo(() => {
|
|
52
|
+
const externalIds = new Map(
|
|
53
|
+
users.map(({ id, external_id }) => [String(id), external_id]),
|
|
54
|
+
);
|
|
55
|
+
return (groupUsers || EMPTY_USERS).map((user) => ({
|
|
56
|
+
...user,
|
|
57
|
+
external_id: user.external_id ?? externalIds.get(String(user.id)),
|
|
58
|
+
}));
|
|
59
|
+
}, [groupUsers, users]);
|
|
47
60
|
const direction = ascending ? "ascending" : "descending";
|
|
48
61
|
const data = _.flow(
|
|
49
62
|
_.trim(filter) ? _.filter(matchesFilter(filter)) : _.identity,
|
|
50
63
|
_.sortBy(accentInsensitivePathOrder(sortBy)),
|
|
51
64
|
ascending ? _.identity : _.reverse,
|
|
52
|
-
)(
|
|
53
|
-
return
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
65
|
+
)(members);
|
|
66
|
+
return (
|
|
67
|
+
<>
|
|
68
|
+
{removing && (
|
|
69
|
+
<GroupDeleteModal
|
|
70
|
+
group={group}
|
|
71
|
+
member={removing}
|
|
72
|
+
onClose={() => setRemoving(null)}
|
|
73
|
+
/>
|
|
74
|
+
)}
|
|
75
|
+
<Table sortable unstackable className="group-users-table">
|
|
76
|
+
<Table.Header>
|
|
77
|
+
<Table.Row>
|
|
78
|
+
{columns.map((columnSpec, key) => (
|
|
79
|
+
<Table.HeaderCell
|
|
80
|
+
key={key}
|
|
81
|
+
width={columnSpec.width}
|
|
82
|
+
content={
|
|
83
|
+
<div className="group-column-heading">
|
|
84
|
+
<span>
|
|
85
|
+
{formatMessage({
|
|
86
|
+
id:
|
|
87
|
+
columnSpec.name === "external_id"
|
|
88
|
+
? "user.form.external_id"
|
|
89
|
+
: `user.props.${columnSpec.header || columnSpec.name}`,
|
|
90
|
+
})}
|
|
91
|
+
</span>
|
|
92
|
+
{columnSpec.name === "full_name" && onFilterChange && (
|
|
93
|
+
<span
|
|
94
|
+
onClick={(event) => event.stopPropagation()}
|
|
95
|
+
onKeyDown={(event) => event.stopPropagation()}
|
|
96
|
+
>
|
|
97
|
+
<SearchInput
|
|
98
|
+
value={filter}
|
|
99
|
+
aria-label={formatMessage({
|
|
100
|
+
id: "user.search.placeholder",
|
|
101
|
+
})}
|
|
102
|
+
placeholder={formatMessage({
|
|
103
|
+
id: "user.search.placeholder",
|
|
104
|
+
})}
|
|
105
|
+
onChange={(_event, { value }) =>
|
|
106
|
+
onFilterChange(value)
|
|
107
|
+
}
|
|
108
|
+
/>
|
|
109
|
+
</span>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
}
|
|
113
|
+
sorted={
|
|
114
|
+
_.pathEq("sort.name", sortBy)(columnSpec) ? direction : null
|
|
115
|
+
}
|
|
116
|
+
className={_.path("sort.name")(columnSpec) ? "" : "disabled"}
|
|
117
|
+
tabIndex={0}
|
|
118
|
+
onKeyDown={(event) => {
|
|
119
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
handleSort(columnSpec);
|
|
122
|
+
}
|
|
123
|
+
}}
|
|
124
|
+
onClick={() => handleSort(columnSpec)}
|
|
125
|
+
/>
|
|
126
|
+
))}
|
|
127
|
+
{!readOnly && <Table.HeaderCell key="delete_action" width={1} />}
|
|
128
|
+
</Table.Row>
|
|
129
|
+
</Table.Header>
|
|
130
|
+
<Table.Body>
|
|
131
|
+
{_.isEmpty(data) && (
|
|
132
|
+
<Table.Row>
|
|
133
|
+
<Table.Cell colSpan={columns.length + (readOnly ? 0 : 1)}>
|
|
134
|
+
{formatMessage({ id: "groupUsers.search.results.empty" })}
|
|
135
|
+
</Table.Cell>
|
|
136
|
+
</Table.Row>
|
|
137
|
+
)}
|
|
138
|
+
{data.map((user) => (
|
|
139
|
+
<Table.Row key={user.id}>
|
|
140
|
+
{columns.map((column, i) => (
|
|
94
141
|
<Table.Cell
|
|
95
142
|
width={_.prop("width")(column)}
|
|
96
143
|
key={i}
|
|
97
144
|
textAlign={column.textAlign}
|
|
98
|
-
content={
|
|
145
|
+
content={
|
|
146
|
+
column.name === "full_name" ? (
|
|
147
|
+
<Link to={linkTo.USER({ id: user.id })}>
|
|
148
|
+
{columnDecorator(column)(user)}
|
|
149
|
+
</Link>
|
|
150
|
+
) : (
|
|
151
|
+
columnDecorator(column)(user)
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
className={
|
|
155
|
+
column.name === "full_name" ? "pointer" : undefined
|
|
156
|
+
}
|
|
99
157
|
/>
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
158
|
+
))}
|
|
159
|
+
{!readOnly && (
|
|
160
|
+
<Table.Cell className="pointer" textAlign="center">
|
|
161
|
+
<Button
|
|
162
|
+
type="button"
|
|
163
|
+
icon
|
|
164
|
+
negative
|
|
165
|
+
basic
|
|
166
|
+
size="mini"
|
|
167
|
+
aria-label={formatMessage({
|
|
168
|
+
id: "groupUsers.actions.delete.confirmation.header",
|
|
169
|
+
})}
|
|
170
|
+
onClick={() => setRemoving(user)}
|
|
171
|
+
>
|
|
172
|
+
<Icon name="trash alternate outline" />
|
|
108
173
|
</Button>
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
onConfirm={() => deleteGroupUser({ user, group, groupUsers })}
|
|
117
|
-
onOpen={(e) => e.stopPropagation()}
|
|
118
|
-
onClose={(e) => e.stopPropagation()}
|
|
119
|
-
/>
|
|
120
|
-
</Table.Cell>
|
|
121
|
-
</Table.Row>
|
|
122
|
-
))}
|
|
123
|
-
</Table.Body>
|
|
124
|
-
</Table>
|
|
174
|
+
</Table.Cell>
|
|
175
|
+
)}
|
|
176
|
+
</Table.Row>
|
|
177
|
+
))}
|
|
178
|
+
</Table.Body>
|
|
179
|
+
</Table>
|
|
180
|
+
</>
|
|
125
181
|
);
|
|
126
182
|
};
|
|
127
183
|
|
|
128
184
|
GroupUsersTable.propTypes = {
|
|
129
185
|
filter: PropTypes.string,
|
|
186
|
+
readOnly: PropTypes.bool,
|
|
187
|
+
onFilterChange: PropTypes.func,
|
|
130
188
|
groupUsers: PropTypes.array,
|
|
131
|
-
|
|
189
|
+
users: PropTypes.array,
|
|
132
190
|
group: PropTypes.object,
|
|
133
191
|
};
|
|
134
192
|
|
|
135
|
-
|
|
136
|
-
groupUsers: state.groupUsers,
|
|
137
|
-
group: state.group,
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
export default connect(mapStateToProps, { deleteGroupUser })(GroupUsersTable);
|
|
193
|
+
export default GroupUsersTable;
|
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useNavigate } from "react-router";
|
|
2
2
|
import { Header, Icon, Container, Segment } from "semantic-ui-react";
|
|
3
|
-
import { connect } from "react-redux";
|
|
4
3
|
import { FormattedMessage, useIntl } from "react-intl";
|
|
5
|
-
import {
|
|
4
|
+
import { GROUPS } from "@truedat/core/routes";
|
|
5
|
+
import { useGroupUsers } from "../hooks/useGroups";
|
|
6
|
+
import { useGroupOperation } from "../hooks/useGroupOperation";
|
|
6
7
|
import GroupForm from "./GroupForm";
|
|
7
8
|
import GroupBreadcrumbs from "./GroupBreadcrumbs";
|
|
8
9
|
|
|
9
|
-
const NewGroup = (
|
|
10
|
+
const NewGroup = () => {
|
|
10
11
|
const { formatMessage } = useIntl();
|
|
12
|
+
const navigate = useNavigate();
|
|
13
|
+
const { users, hasData, loading, error, retry } = useGroupUsers();
|
|
14
|
+
const { submit, pending, failed } = useGroupOperation("create", null, () =>
|
|
15
|
+
navigate(GROUPS),
|
|
16
|
+
);
|
|
11
17
|
return (
|
|
12
18
|
<>
|
|
13
19
|
<GroupBreadcrumbs name="groups.actions.create" />
|
|
@@ -26,14 +32,36 @@ const NewGroup = ({ createGroup }) => {
|
|
|
26
32
|
/>
|
|
27
33
|
</Header.Content>
|
|
28
34
|
</Header>
|
|
29
|
-
|
|
35
|
+
{loading && !hasData ? (
|
|
36
|
+
<p role="status">Cargando usuarios…</p>
|
|
37
|
+
) : !hasData ? (
|
|
38
|
+
<div role="alert">
|
|
39
|
+
No se han podido cargar los usuarios.
|
|
40
|
+
<button type="button" onClick={() => retry().catch(() => {})}>
|
|
41
|
+
Reintentar
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
) : (
|
|
45
|
+
<>
|
|
46
|
+
{error && (
|
|
47
|
+
<div role="alert">
|
|
48
|
+
No se han podido actualizar los usuarios.
|
|
49
|
+
<button type="button" onClick={() => retry().catch(() => {})}>
|
|
50
|
+
Reintentar
|
|
51
|
+
</button>
|
|
52
|
+
</div>
|
|
53
|
+
)}
|
|
54
|
+
{failed && (
|
|
55
|
+
<p role="alert">
|
|
56
|
+
No se ha podido crear el grupo. Inténtalo de nuevo.
|
|
57
|
+
</p>
|
|
58
|
+
)}
|
|
59
|
+
<GroupForm onSubmit={submit} users={users} isSubmitting={pending} />
|
|
60
|
+
</>
|
|
61
|
+
)}
|
|
30
62
|
</Container>
|
|
31
63
|
</>
|
|
32
64
|
);
|
|
33
65
|
};
|
|
34
66
|
|
|
35
|
-
|
|
36
|
-
createGroup: PropTypes.func,
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export default connect(null, { createGroup })(NewGroup);
|
|
67
|
+
export default NewGroup;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { useParams } from "react-router";
|
|
2
|
+
import { SWRConfig, useSWRConfig } from "swr";
|
|
3
|
+
import { act, waitFor } from "@testing-library/react";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
5
|
+
import { render } from "@truedat/test/render";
|
|
6
|
+
import { apiJson } from "@truedat/core/services/api";
|
|
7
|
+
import EditGroup from "../EditGroup";
|
|
8
|
+
|
|
9
|
+
jest.mock("react-router", () => ({
|
|
10
|
+
...jest.requireActual("react-router"),
|
|
11
|
+
useParams: jest.fn(),
|
|
12
|
+
useNavigate: () => jest.fn(),
|
|
13
|
+
}));
|
|
14
|
+
jest.mock("@truedat/core/services/api", () => ({
|
|
15
|
+
apiJson: jest.fn(),
|
|
16
|
+
apiJsonPut: jest.fn(),
|
|
17
|
+
JSON_OPTS: {},
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
const groups = {
|
|
21
|
+
1: { id: 1, name: "Team one", users: [{ id: 11 }] },
|
|
22
|
+
2: { id: 2, name: "Team two", users: [{ id: 22 }] },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const RefreshGroup = () => {
|
|
26
|
+
const { mutate } = useSWRConfig();
|
|
27
|
+
return (
|
|
28
|
+
<button type="button" onClick={() => mutate("/api/groups/1")}>
|
|
29
|
+
Refresh group
|
|
30
|
+
</button>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const setup = () => {
|
|
35
|
+
const cache = new Map();
|
|
36
|
+
const swrConfig = { provider: () => cache, dedupingInterval: 0 };
|
|
37
|
+
return {
|
|
38
|
+
cache,
|
|
39
|
+
swrConfig,
|
|
40
|
+
ui: render(
|
|
41
|
+
<SWRConfig value={swrConfig}>
|
|
42
|
+
<RefreshGroup />
|
|
43
|
+
<EditGroup />
|
|
44
|
+
</SWRConfig>,
|
|
45
|
+
),
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
describe("<EditGroup />", () => {
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
jest.resetAllMocks();
|
|
52
|
+
useParams.mockReturnValue({ id: "1" });
|
|
53
|
+
apiJson.mockImplementation((url) =>
|
|
54
|
+
Promise.resolve({
|
|
55
|
+
data: {
|
|
56
|
+
data:
|
|
57
|
+
url === "/api/users"
|
|
58
|
+
? []
|
|
59
|
+
: url === "/api/groups"
|
|
60
|
+
? Object.values(groups)
|
|
61
|
+
: groups[url.split("/").pop()],
|
|
62
|
+
},
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("reinitializes the form when navigating between cached groups", async () => {
|
|
68
|
+
const { ui, swrConfig } = setup();
|
|
69
|
+
const user = userEvent.setup({ delay: null });
|
|
70
|
+
const name = await ui.findByRole("textbox", { name: "group.props.name" });
|
|
71
|
+
await user.clear(name);
|
|
72
|
+
await user.type(name, "Unsaved team one");
|
|
73
|
+
|
|
74
|
+
useParams.mockReturnValue({ id: "2" });
|
|
75
|
+
ui.rerender(
|
|
76
|
+
<SWRConfig value={swrConfig}>
|
|
77
|
+
<EditGroup />
|
|
78
|
+
</SWRConfig>,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
await waitFor(() =>
|
|
82
|
+
expect(ui.getByRole("textbox", { name: "group.props.name" })).toHaveValue(
|
|
83
|
+
"Team two",
|
|
84
|
+
),
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("keeps an edit draft when a background group refresh fails", async () => {
|
|
89
|
+
const { ui } = setup();
|
|
90
|
+
const user = userEvent.setup({ delay: null });
|
|
91
|
+
const name = await ui.findByRole("textbox", { name: "group.props.name" });
|
|
92
|
+
await user.clear(name);
|
|
93
|
+
await user.type(name, "Unsaved draft");
|
|
94
|
+
|
|
95
|
+
apiJson.mockRejectedValue(new Error("offline"));
|
|
96
|
+
await user.click(ui.getByRole("button", { name: "Refresh group" }));
|
|
97
|
+
expect(
|
|
98
|
+
await ui.findByText(
|
|
99
|
+
"No se ha podido actualizar el grupo o sus usuarios.",
|
|
100
|
+
),
|
|
101
|
+
).toBeInTheDocument();
|
|
102
|
+
|
|
103
|
+
expect(name).toHaveValue("Unsaved draft");
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -1,15 +1,23 @@
|
|
|
1
|
-
import { fireEvent, waitFor } from "@testing-library/react";
|
|
2
1
|
import userEvent from "@testing-library/user-event";
|
|
2
|
+
import { waitFor } from "@testing-library/react";
|
|
3
3
|
import { render } from "@truedat/test/render";
|
|
4
|
-
import
|
|
4
|
+
import { useGroup } from "../../hooks/useGroups";
|
|
5
|
+
import { Group } from "../Group";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
jest.mock("../../hooks/useGroups", () => ({ useGroup: jest.fn() }));
|
|
8
|
+
jest.mock("../GroupDeleteModal", () => ({ group }) => (
|
|
9
|
+
<div>Delete group {group.name}</div>
|
|
10
|
+
));
|
|
11
|
+
|
|
12
|
+
const group = { id: "2", name: "grupo1 ", description: "aaa bbb cc" };
|
|
7
13
|
|
|
8
14
|
describe("<Group />", () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
useGroup.mockReturnValue({ group, loading: false, retry: jest.fn() });
|
|
17
|
+
});
|
|
18
|
+
|
|
9
19
|
it("matches the latest snapshot", () => {
|
|
10
|
-
const { container } = render(
|
|
11
|
-
<Group group={group} deleteGroup={jest.fn()} groupLoading={false} />,
|
|
12
|
-
);
|
|
20
|
+
const { container } = render(<Group />);
|
|
13
21
|
expect(container).toMatchSnapshot();
|
|
14
22
|
});
|
|
15
23
|
|
|
@@ -20,58 +28,32 @@ describe("<Group />", () => {
|
|
|
20
28
|
alias: "Grupo Alias",
|
|
21
29
|
description: "aaa bbb cc",
|
|
22
30
|
};
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
);
|
|
31
|
+
useGroup.mockReturnValue({ group, loading: false, retry: jest.fn() });
|
|
32
|
+
const { getByText } = render(<Group />);
|
|
26
33
|
|
|
27
34
|
expect(getByText("Grupo Alias")).toBeInTheDocument();
|
|
28
35
|
expect(getByText("grupo1")).toBeInTheDocument();
|
|
29
36
|
});
|
|
30
37
|
|
|
31
|
-
it("
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
);
|
|
35
|
-
expect(rendered.container).toBeEmptyDOMElement();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("renders nothing when the group has no id", () => {
|
|
39
|
-
const rendered = render(
|
|
40
|
-
<Group
|
|
41
|
-
group={{ name: "no id", description: "x" }}
|
|
42
|
-
deleteGroup={jest.fn()}
|
|
43
|
-
groupLoading={false}
|
|
44
|
-
/>,
|
|
45
|
-
);
|
|
46
|
-
expect(rendered.container).toBeEmptyDOMElement();
|
|
38
|
+
it("shows a loading status while the group is loading", () => {
|
|
39
|
+
useGroup.mockReturnValue({ group: undefined, loading: true });
|
|
40
|
+
const rendered = render(<Group />);
|
|
41
|
+
expect(rendered.getByRole("status")).toBeInTheDocument();
|
|
47
42
|
});
|
|
48
43
|
|
|
49
|
-
it("
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
groupLoading: false,
|
|
54
|
-
},
|
|
55
|
-
});
|
|
56
|
-
expect(rendered.getByText(/from store/i)).toBeInTheDocument();
|
|
44
|
+
it("shows an unavailable message when the group cannot be loaded", () => {
|
|
45
|
+
useGroup.mockReturnValue({ group: undefined, loading: false });
|
|
46
|
+
const rendered = render(<Group />);
|
|
47
|
+
expect(rendered.getByRole("alert")).toBeInTheDocument();
|
|
57
48
|
});
|
|
58
49
|
|
|
59
|
-
it("
|
|
50
|
+
it("opens the delete dialog from the group actions menu", async () => {
|
|
60
51
|
const user = userEvent.setup({ delay: null });
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
);
|
|
65
|
-
const dropdown = rendered.getByRole("listbox");
|
|
66
|
-
fireEvent.click(dropdown);
|
|
67
|
-
await waitFor(() =>
|
|
68
|
-
expect(rendered.getByText(/actions.delete/i)).toBeInTheDocument(),
|
|
69
|
-
);
|
|
70
|
-
await user.click(rendered.getByText(/actions.delete/i));
|
|
52
|
+
const rendered = render(<Group />);
|
|
53
|
+
await user.click(rendered.getByRole("listbox"));
|
|
54
|
+
await user.click(rendered.getByText("actions.delete"));
|
|
71
55
|
await waitFor(() =>
|
|
72
|
-
expect(rendered.getByText(/
|
|
56
|
+
expect(rendered.getByText(/Delete group grupo1/)).toBeInTheDocument(),
|
|
73
57
|
);
|
|
74
|
-
await user.click(rendered.getByText(/confirmation.yes/i));
|
|
75
|
-
expect(deleteGroup).toHaveBeenCalledWith({ id: group.id });
|
|
76
58
|
});
|
|
77
59
|
});
|
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
import userEvent from "@testing-library/user-event";
|
|
2
1
|
import { render, waitForLoad } from "@truedat/test/render";
|
|
3
2
|
import { GroupCard } from "../GroupCard";
|
|
4
3
|
|
|
5
4
|
describe("<GroupCard />", () => {
|
|
6
|
-
const deleteGroup = jest.fn();
|
|
7
5
|
const group = {
|
|
8
6
|
id: "1",
|
|
9
7
|
name: "administrators",
|
|
10
8
|
description: "Administrators",
|
|
11
9
|
};
|
|
12
|
-
const props = { group
|
|
10
|
+
const props = { group };
|
|
13
11
|
|
|
14
12
|
it("matches the latest snapshot", async () => {
|
|
15
13
|
const rendered = render(<GroupCard {...props} />);
|
|
@@ -22,27 +20,17 @@ describe("<GroupCard />", () => {
|
|
|
22
20
|
...group,
|
|
23
21
|
alias: "Admins",
|
|
24
22
|
};
|
|
25
|
-
const rendered = render(
|
|
26
|
-
<GroupCard group={aliasGroup} deleteGroup={deleteGroup} />,
|
|
27
|
-
);
|
|
23
|
+
const rendered = render(<GroupCard group={aliasGroup} />);
|
|
28
24
|
await waitForLoad(rendered);
|
|
29
25
|
|
|
30
26
|
expect(rendered.getByText("Admins")).toBeInTheDocument();
|
|
31
|
-
expect(rendered.
|
|
27
|
+
expect(rendered.getByText("administrators")).toBeInTheDocument();
|
|
32
28
|
});
|
|
33
29
|
|
|
34
|
-
it("
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
await user.click(rendered.getByRole("button", { name: /delete/i }));
|
|
41
|
-
|
|
42
|
-
await user.click(
|
|
43
|
-
rendered.getByRole("button", { name: /modal-affirmative-action/i }),
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
expect(deleteGroup).toHaveBeenCalledTimes(1);
|
|
30
|
+
it("does not offer deletion from the card", () => {
|
|
31
|
+
const ui = render(<GroupCard {...props} />);
|
|
32
|
+
expect(
|
|
33
|
+
ui.queryByRole("button", { name: /delete/i }),
|
|
34
|
+
).not.toBeInTheDocument();
|
|
47
35
|
});
|
|
48
36
|
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { fireEvent } from "@testing-library/react";
|
|
2
|
+
import { render } from "@truedat/test/render";
|
|
3
|
+
import GroupDropdown from "../GroupDropdown";
|
|
4
|
+
|
|
5
|
+
it("closes only the dropdown with Escape and restores the menu styles", () => {
|
|
6
|
+
const ui = render(
|
|
7
|
+
<GroupDropdown
|
|
8
|
+
selection
|
|
9
|
+
aria-label="Filter"
|
|
10
|
+
options={[{ key: 1, value: 1, text: "One" }]}
|
|
11
|
+
/>,
|
|
12
|
+
);
|
|
13
|
+
const trigger = ui.getByRole("listbox", { name: "Filter" });
|
|
14
|
+
fireEvent.click(trigger);
|
|
15
|
+
const menu = trigger.querySelector(".menu");
|
|
16
|
+
expect(menu).toHaveAttribute("popover", "manual");
|
|
17
|
+
expect(menu.style.position).toBe("fixed");
|
|
18
|
+
const outerEscape = jest.fn();
|
|
19
|
+
document.addEventListener("keydown", outerEscape);
|
|
20
|
+
fireEvent.keyDown(trigger, { key: "Escape" });
|
|
21
|
+
expect(outerEscape).not.toHaveBeenCalled();
|
|
22
|
+
expect(menu).not.toHaveAttribute("popover");
|
|
23
|
+
expect(menu.style.position).toBe("");
|
|
24
|
+
fireEvent.keyDown(trigger, { key: "Escape" });
|
|
25
|
+
expect(outerEscape).toHaveBeenCalledTimes(1);
|
|
26
|
+
document.removeEventListener("keydown", outerEscape);
|
|
27
|
+
});
|