@truedat/auth 8.14.4 → 8.14.5
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/GroupCard.js +2 -2
- package/src/groups/components/GroupCards.js +129 -37
- package/src/groups/components/GroupForm.js +13 -7
- package/src/groups/components/GroupFormModal.js +8 -7
- package/src/groups/components/GroupInfoModal.js +6 -6
- package/src/groups/components/GroupMemberPicker.js +2 -4
- package/src/groups/components/GroupRouteModal.js +15 -10
- package/src/groups/components/__tests__/GroupCards.spec.js +94 -0
- package/src/groups/components/__tests__/GroupForm.spec.js +2 -2
- package/src/groups/components/__tests__/GroupModals.spec.js +42 -16
- package/src/groups/components/__tests__/__snapshots__/GroupCards.spec.js.snap +14 -0
- package/src/styles/Groups.less +96 -131
- package/src/styles/UserCard.less +60 -226
- package/src/styles/UserModals.less +6 -1
- package/src/styles/Users.less +40 -7
- package/src/users/components/EditUserModal.js +3 -3
- package/src/users/components/UserAcls.js +61 -0
- package/src/users/components/UserCard.js +4 -1
- package/src/users/components/UserCards.js +35 -120
- package/src/users/components/UserInfoModal.js +22 -62
- package/src/users/components/__tests__/EditUser.spec.js +1 -1
- package/src/users/components/__tests__/UserAcls.spec.js +40 -1
- package/src/users/components/__tests__/UserCard.spec.js +1 -1
- package/src/users/components/__tests__/UserCards.spec.js +0 -45
- package/src/users/components/__tests__/UserModals.spec.js +54 -7
- package/src/users/components/__tests__/__snapshots__/UserCard.spec.js.snap +2 -0
- package/src/users/components/__tests__/__snapshots__/UserCards.spec.js.snap +20 -69
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@truedat/auth",
|
|
3
|
-
"version": "8.14.
|
|
3
|
+
"version": "8.14.5",
|
|
4
4
|
"description": "Truedat Web Auth",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"jsnext:main": "src/index.js",
|
|
@@ -54,14 +54,14 @@
|
|
|
54
54
|
"@testing-library/jest-dom": "^6.6.3",
|
|
55
55
|
"@testing-library/react": "^16.3.0",
|
|
56
56
|
"@testing-library/user-event": "^14.6.1",
|
|
57
|
-
"@truedat/test": "8.14.
|
|
57
|
+
"@truedat/test": "8.14.5",
|
|
58
58
|
"identity-obj-proxy": "^3.0.0",
|
|
59
59
|
"jest": "^29.7.0",
|
|
60
60
|
"redux-saga-test-plan": "^4.0.6"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
63
|
"@apollo/client": "^3.13.8",
|
|
64
|
-
"@truedat/core": "8.14.
|
|
64
|
+
"@truedat/core": "8.14.5",
|
|
65
65
|
"auth0-js": "^10.2.0",
|
|
66
66
|
"axios": "^1.20.0",
|
|
67
67
|
"graphql": "^16.11.0",
|
|
@@ -90,5 +90,5 @@
|
|
|
90
90
|
"swr": "^2.3.3"
|
|
91
91
|
},
|
|
92
92
|
"overrides": {},
|
|
93
|
-
"gitHead": "
|
|
93
|
+
"gitHead": "a792103a77b3b2392be5777a8dc6675ac3882ffd"
|
|
94
94
|
}
|
|
@@ -45,8 +45,8 @@ export const GroupCard = ({ group }) => {
|
|
|
45
45
|
<span className="gc-count-label">
|
|
46
46
|
{formatMessage(
|
|
47
47
|
isEmpty
|
|
48
|
-
? { id: "group.members.none", defaultMessage: "
|
|
49
|
-
: { id: "navigation.members", defaultMessage: "
|
|
48
|
+
? { id: "group.members.none", defaultMessage: "No members" }
|
|
49
|
+
: { id: "navigation.members", defaultMessage: "members" },
|
|
50
50
|
)}
|
|
51
51
|
</span>
|
|
52
52
|
</span>
|
|
@@ -1,42 +1,137 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
1
2
|
import PropTypes from "prop-types";
|
|
2
3
|
import { Link } from "react-router";
|
|
3
|
-
import { Button, Icon } from "semantic-ui-react";
|
|
4
|
-
import { FormattedMessage } from "react-intl";
|
|
4
|
+
import { Button, Icon, Input, Message, Popup } from "semantic-ui-react";
|
|
5
|
+
import { FormattedMessage, useIntl } from "react-intl";
|
|
6
|
+
import { lowerDeburrTrim } from "@truedat/core/services/sort";
|
|
5
7
|
import { GROUP_CREATE } from "@truedat/core/routes";
|
|
6
8
|
import { useGroups } from "../hooks/useGroups";
|
|
7
9
|
import GroupCard from "./GroupCard";
|
|
8
10
|
|
|
9
|
-
export const GroupCards = ({ groups = [] }) =>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
11
|
+
export const GroupCards = ({ groups = [] }) => {
|
|
12
|
+
const [search, setSearch] = useState("");
|
|
13
|
+
const [hintOpen, setHintOpen] = useState(false);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
try {
|
|
16
|
+
setHintOpen(
|
|
17
|
+
localStorage.getItem("truedat.groups.search-hint.dismissed") !== "true",
|
|
18
|
+
);
|
|
19
|
+
} catch {
|
|
20
|
+
setHintOpen(true);
|
|
21
|
+
}
|
|
22
|
+
}, []);
|
|
23
|
+
const { formatMessage } = useIntl();
|
|
24
|
+
const query = lowerDeburrTrim(search);
|
|
25
|
+
const filteredGroups = query
|
|
26
|
+
? groups.filter((group) =>
|
|
27
|
+
[
|
|
28
|
+
group.name,
|
|
29
|
+
group.alias,
|
|
30
|
+
group.description,
|
|
31
|
+
...(group.users || []).flatMap((user) => [
|
|
32
|
+
user.user_name,
|
|
33
|
+
user.full_name,
|
|
34
|
+
]),
|
|
35
|
+
].some((value) => lowerDeburrTrim(value).includes(query)),
|
|
36
|
+
)
|
|
37
|
+
: groups;
|
|
38
|
+
const searchLabel = formatMessage({
|
|
39
|
+
id: "groups.search.placeholder",
|
|
40
|
+
defaultMessage: "Search groups or users…",
|
|
41
|
+
});
|
|
42
|
+
return (
|
|
43
|
+
<>
|
|
44
|
+
<div className="groups-toolbar">
|
|
45
|
+
<Popup
|
|
46
|
+
className="groups-search-hint-popup"
|
|
47
|
+
content={
|
|
48
|
+
<div className="groups-search-hint">
|
|
49
|
+
<FormattedMessage
|
|
50
|
+
id="groups.search.hint"
|
|
51
|
+
defaultMessage="You can now <emphasis>also</emphasis> search for users within groups"
|
|
52
|
+
values={{
|
|
53
|
+
emphasis: (text) => (
|
|
54
|
+
<strong>
|
|
55
|
+
<em>{text}</em>
|
|
56
|
+
</strong>
|
|
57
|
+
),
|
|
58
|
+
}}
|
|
59
|
+
/>
|
|
60
|
+
<Button
|
|
61
|
+
icon
|
|
62
|
+
basic
|
|
63
|
+
className="groups-search-hint-close"
|
|
64
|
+
aria-label={formatMessage({
|
|
65
|
+
id: "actions.close",
|
|
66
|
+
defaultMessage: "Close",
|
|
67
|
+
})}
|
|
68
|
+
onClick={() => {
|
|
69
|
+
setHintOpen(false);
|
|
70
|
+
try {
|
|
71
|
+
localStorage.setItem(
|
|
72
|
+
"truedat.groups.search-hint.dismissed",
|
|
73
|
+
"true",
|
|
74
|
+
);
|
|
75
|
+
} catch {
|
|
76
|
+
// Keep the hint dismissed for this render if storage is unavailable.
|
|
77
|
+
}
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
<Icon name="close" />
|
|
81
|
+
</Button>
|
|
82
|
+
</div>
|
|
83
|
+
}
|
|
84
|
+
open={hintOpen}
|
|
85
|
+
on={[]}
|
|
86
|
+
position="top left"
|
|
87
|
+
trigger={
|
|
88
|
+
<Input
|
|
89
|
+
icon="search"
|
|
90
|
+
iconPosition="left"
|
|
91
|
+
value={search}
|
|
92
|
+
onChange={(_event, { value }) => setSearch(value)}
|
|
93
|
+
placeholder={searchLabel}
|
|
94
|
+
aria-label={searchLabel}
|
|
27
95
|
/>
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
96
|
+
}
|
|
97
|
+
/>
|
|
98
|
+
<Button
|
|
99
|
+
primary
|
|
100
|
+
as={Link}
|
|
101
|
+
to={GROUP_CREATE}
|
|
102
|
+
className="group-create-button"
|
|
103
|
+
>
|
|
104
|
+
<span className="group-create-icon" aria-hidden="true">
|
|
105
|
+
<Icon name="users" />
|
|
106
|
+
<svg viewBox="0 0 16 16" focusable="false">
|
|
107
|
+
<path
|
|
108
|
+
d="M8 3v10M3 8h10"
|
|
109
|
+
fill="none"
|
|
110
|
+
stroke="currentColor"
|
|
111
|
+
strokeWidth="2"
|
|
112
|
+
strokeLinecap="round"
|
|
113
|
+
/>
|
|
114
|
+
</svg>
|
|
115
|
+
</span>
|
|
116
|
+
<FormattedMessage id="groups.actions.create" />
|
|
117
|
+
</Button>
|
|
118
|
+
</div>
|
|
119
|
+
<div className="groups-card-list">
|
|
120
|
+
{filteredGroups.map((group) => (
|
|
121
|
+
<GroupCard key={group.id} group={group} />
|
|
122
|
+
))}
|
|
123
|
+
</div>
|
|
124
|
+
{query && filteredGroups.length === 0 && (
|
|
125
|
+
<Message role="status">
|
|
126
|
+
<FormattedMessage
|
|
127
|
+
id="groups.search.no_results"
|
|
128
|
+
defaultMessage="No groups found for this search."
|
|
129
|
+
/>
|
|
130
|
+
</Message>
|
|
131
|
+
)}
|
|
132
|
+
</>
|
|
133
|
+
);
|
|
134
|
+
};
|
|
40
135
|
GroupCards.propTypes = { groups: PropTypes.array };
|
|
41
136
|
export const GroupsList = () => {
|
|
42
137
|
const { groups, error, loading, retry } = useGroups();
|
|
@@ -46,7 +141,7 @@ export const GroupsList = () => {
|
|
|
46
141
|
<p role="status">
|
|
47
142
|
<FormattedMessage
|
|
48
143
|
id="group.loading"
|
|
49
|
-
defaultMessage="
|
|
144
|
+
defaultMessage="Loading groups…"
|
|
50
145
|
/>
|
|
51
146
|
</p>
|
|
52
147
|
)}
|
|
@@ -54,10 +149,7 @@ export const GroupsList = () => {
|
|
|
54
149
|
<div role="alert">
|
|
55
150
|
<FormattedMessage id="group.unavailable" />
|
|
56
151
|
<Button onClick={() => retry().catch(() => {})}>
|
|
57
|
-
<FormattedMessage
|
|
58
|
-
id="group.members.retry"
|
|
59
|
-
defaultMessage="Reintentar"
|
|
60
|
-
/>
|
|
152
|
+
<FormattedMessage id="group.members.retry" defaultMessage="Retry" />
|
|
61
153
|
</Button>
|
|
62
154
|
</div>
|
|
63
155
|
)}
|
|
@@ -30,6 +30,7 @@ export const GroupForm = ({
|
|
|
30
30
|
onCancel,
|
|
31
31
|
onDelete,
|
|
32
32
|
memberFilters = false,
|
|
33
|
+
editingModal = false,
|
|
33
34
|
}) => {
|
|
34
35
|
const { formatMessage } = useIntl();
|
|
35
36
|
const nameFieldId = useId();
|
|
@@ -74,7 +75,7 @@ export const GroupForm = ({
|
|
|
74
75
|
memberFilters
|
|
75
76
|
? {
|
|
76
77
|
id: "group.form.name.label",
|
|
77
|
-
defaultMessage: "
|
|
78
|
+
defaultMessage: "Group name",
|
|
78
79
|
}
|
|
79
80
|
: { id: "group.props.name" },
|
|
80
81
|
)}
|
|
@@ -115,7 +116,7 @@ export const GroupForm = ({
|
|
|
115
116
|
message: formatMessage({
|
|
116
117
|
id: "group.description.maxLength",
|
|
117
118
|
defaultMessage:
|
|
118
|
-
"
|
|
119
|
+
"The description cannot exceed 55 characters.",
|
|
119
120
|
}),
|
|
120
121
|
},
|
|
121
122
|
}
|
|
@@ -131,14 +132,14 @@ export const GroupForm = ({
|
|
|
131
132
|
? "group.description.short"
|
|
132
133
|
: "group.props.description",
|
|
133
134
|
defaultMessage: memberFilters
|
|
134
|
-
? "
|
|
135
|
-
: "
|
|
135
|
+
? "Short description"
|
|
136
|
+
: "Description",
|
|
136
137
|
})}
|
|
137
138
|
onBlur={onBlur}
|
|
138
139
|
onChange={(_e, { value }) => onChange(value)}
|
|
139
140
|
placeholder={formatMessage({
|
|
140
141
|
id: "group.form.description.placeholder",
|
|
141
|
-
defaultMessage: "
|
|
142
|
+
defaultMessage: "Group description",
|
|
142
143
|
})}
|
|
143
144
|
value={value || ""}
|
|
144
145
|
/>
|
|
@@ -196,7 +197,9 @@ export const GroupForm = ({
|
|
|
196
197
|
secondary
|
|
197
198
|
disabled={isSubmitting}
|
|
198
199
|
onClick={onCancel}
|
|
199
|
-
content={formatMessage({
|
|
200
|
+
content={formatMessage({
|
|
201
|
+
id: editingModal ? "actions.close" : "actions.cancel",
|
|
202
|
+
})}
|
|
200
203
|
/>
|
|
201
204
|
) : (
|
|
202
205
|
<HistoryBackButton
|
|
@@ -209,7 +212,9 @@ export const GroupForm = ({
|
|
|
209
212
|
primary
|
|
210
213
|
loading={isSubmitting}
|
|
211
214
|
disabled={isSubmitting || !isDirty || !isValid}
|
|
212
|
-
content={formatMessage({
|
|
215
|
+
content={formatMessage({
|
|
216
|
+
id: editingModal ? "actions.edit" : "actions.save",
|
|
217
|
+
})}
|
|
213
218
|
/>
|
|
214
219
|
</div>
|
|
215
220
|
</Form>
|
|
@@ -219,6 +224,7 @@ export const GroupForm = ({
|
|
|
219
224
|
GroupForm.propTypes = {
|
|
220
225
|
users: PropTypes.array,
|
|
221
226
|
memberFilters: PropTypes.bool,
|
|
227
|
+
editingModal: PropTypes.bool,
|
|
222
228
|
group: PropTypes.object,
|
|
223
229
|
groupUsers: PropTypes.array,
|
|
224
230
|
onSubmit: PropTypes.func,
|
|
@@ -46,7 +46,7 @@ const GroupFormModal = ({ editing = false, onClose }) => {
|
|
|
46
46
|
? formatMessage(
|
|
47
47
|
{
|
|
48
48
|
id: "group.edit.title",
|
|
49
|
-
defaultMessage: "
|
|
49
|
+
defaultMessage: "Edit group {name}",
|
|
50
50
|
},
|
|
51
51
|
{ name: group?.name || "" },
|
|
52
52
|
)
|
|
@@ -62,7 +62,7 @@ const GroupFormModal = ({ editing = false, onClose }) => {
|
|
|
62
62
|
? formatMessage({
|
|
63
63
|
id: "group.operation.error",
|
|
64
64
|
defaultMessage:
|
|
65
|
-
"
|
|
65
|
+
"The operation could not be completed. Please try again.",
|
|
66
66
|
})
|
|
67
67
|
: null
|
|
68
68
|
}
|
|
@@ -72,6 +72,7 @@ const GroupFormModal = ({ editing = false, onClose }) => {
|
|
|
72
72
|
<GroupForm
|
|
73
73
|
key={editing ? group.id : "new"}
|
|
74
74
|
memberFilters
|
|
75
|
+
editingModal={editing}
|
|
75
76
|
group={editing ? group : {}}
|
|
76
77
|
groupUsers={editing ? groupUsers : []}
|
|
77
78
|
users={users}
|
|
@@ -89,8 +90,8 @@ const GroupFormModal = ({ editing = false, onClose }) => {
|
|
|
89
90
|
loading || usersLoading ? "group.loading" : "group.unavailable",
|
|
90
91
|
defaultMessage:
|
|
91
92
|
loading || usersLoading
|
|
92
|
-
? "
|
|
93
|
-
: "
|
|
93
|
+
? "Loading group…"
|
|
94
|
+
: "The group is unavailable.",
|
|
94
95
|
})}
|
|
95
96
|
</p>
|
|
96
97
|
)}
|
|
@@ -98,12 +99,12 @@ const GroupFormModal = ({ editing = false, onClose }) => {
|
|
|
98
99
|
<div role="alert">
|
|
99
100
|
{formatMessage({
|
|
100
101
|
id: "group.members.error",
|
|
101
|
-
defaultMessage: "
|
|
102
|
+
defaultMessage: "Could not load users.",
|
|
102
103
|
})}
|
|
103
104
|
<button type="button" onClick={() => retryUsers().catch(() => {})}>
|
|
104
105
|
{formatMessage({
|
|
105
106
|
id: "group.members.retry",
|
|
106
|
-
defaultMessage: "
|
|
107
|
+
defaultMessage: "Retry",
|
|
107
108
|
})}
|
|
108
109
|
</button>
|
|
109
110
|
</div>
|
|
@@ -114,7 +115,7 @@ const GroupFormModal = ({ editing = false, onClose }) => {
|
|
|
114
115
|
<button type="button" onClick={() => retry().catch(() => {})}>
|
|
115
116
|
{formatMessage({
|
|
116
117
|
id: "group.members.retry",
|
|
117
|
-
defaultMessage: "
|
|
118
|
+
defaultMessage: "Retry",
|
|
118
119
|
})}
|
|
119
120
|
</button>
|
|
120
121
|
</div>
|
|
@@ -29,7 +29,7 @@ const GroupInfoModal = ({ onClose }) => {
|
|
|
29
29
|
? group.name
|
|
30
30
|
: formatMessage({
|
|
31
31
|
id: "group.loading",
|
|
32
|
-
defaultMessage: "
|
|
32
|
+
defaultMessage: "Loading group…",
|
|
33
33
|
})
|
|
34
34
|
}
|
|
35
35
|
subtitle={ready ? group.description || "" : ""}
|
|
@@ -60,7 +60,7 @@ const GroupInfoModal = ({ onClose }) => {
|
|
|
60
60
|
<Button type="button" onClick={() => retry().catch(() => {})}>
|
|
61
61
|
{formatMessage({
|
|
62
62
|
id: "group.members.retry",
|
|
63
|
-
defaultMessage: "
|
|
63
|
+
defaultMessage: "Retry",
|
|
64
64
|
})}
|
|
65
65
|
</Button>
|
|
66
66
|
</div>
|
|
@@ -69,8 +69,8 @@ const GroupInfoModal = ({ onClose }) => {
|
|
|
69
69
|
{formatMessage({
|
|
70
70
|
id: loading ? "group.loading" : "group.unavailable",
|
|
71
71
|
defaultMessage: loading
|
|
72
|
-
? "
|
|
73
|
-
: "
|
|
72
|
+
? "Loading group…"
|
|
73
|
+
: "The group is unavailable.",
|
|
74
74
|
})}
|
|
75
75
|
</p>
|
|
76
76
|
) : empty ? (
|
|
@@ -79,13 +79,13 @@ const GroupInfoModal = ({ onClose }) => {
|
|
|
79
79
|
<h3>
|
|
80
80
|
{formatMessage({
|
|
81
81
|
id: "group.members.empty.title",
|
|
82
|
-
defaultMessage: "
|
|
82
|
+
defaultMessage: "This group has no members",
|
|
83
83
|
})}
|
|
84
84
|
</h3>
|
|
85
85
|
<p>
|
|
86
86
|
{formatMessage({
|
|
87
87
|
id: "group.members.empty.description",
|
|
88
|
-
defaultMessage: "
|
|
88
|
+
defaultMessage: "Edit the group to add members.",
|
|
89
89
|
})}
|
|
90
90
|
</p>
|
|
91
91
|
<Button
|
|
@@ -310,7 +310,7 @@ const GroupMemberPicker = ({
|
|
|
310
310
|
aria-label={formatMessage(
|
|
311
311
|
{
|
|
312
312
|
id: "group.members.remove",
|
|
313
|
-
defaultMessage: "
|
|
313
|
+
defaultMessage: "Remove {name} from the group",
|
|
314
314
|
},
|
|
315
315
|
{ name },
|
|
316
316
|
)}
|
|
@@ -325,9 +325,7 @@ const GroupMemberPicker = ({
|
|
|
325
325
|
})}
|
|
326
326
|
</ul>
|
|
327
327
|
) : (
|
|
328
|
-
<p>
|
|
329
|
-
{message("group.members.empty", "No hay miembros seleccionados")}
|
|
330
|
-
</p>
|
|
328
|
+
<p>{message("group.members.empty", "No members selected")}</p>
|
|
331
329
|
)}
|
|
332
330
|
</section>
|
|
333
331
|
{showFilters && failed && (
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import PropTypes from "prop-types";
|
|
2
|
-
import { useCallback, useEffect } from "react";
|
|
2
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
3
3
|
import { useNavigate, useLocation, useParams } from "react-router";
|
|
4
4
|
import { GROUPS } from "@truedat/core/routes";
|
|
5
5
|
import GroupFormModal from "./GroupFormModal";
|
|
@@ -9,11 +9,14 @@ const GroupRouteModal = ({ mode }) => {
|
|
|
9
9
|
const navigate = useNavigate();
|
|
10
10
|
const { pathname } = useLocation();
|
|
11
11
|
const { id } = useParams();
|
|
12
|
+
const keyboardClose = useRef(false);
|
|
12
13
|
useEffect(() => {
|
|
13
14
|
const opener = document.activeElement;
|
|
15
|
+
const keyboardFocus = opener?.matches(":focus-visible");
|
|
14
16
|
return () => {
|
|
17
|
+
const restoreFocus = keyboardFocus || keyboardClose.current;
|
|
15
18
|
requestAnimationFrame(() => {
|
|
16
|
-
if (document.querySelector(".group-modal")) return;
|
|
19
|
+
if (!restoreFocus || document.querySelector(".group-modal")) return;
|
|
17
20
|
if (opener?.isConnected && opener !== document.body) opener.focus();
|
|
18
21
|
else
|
|
19
22
|
document
|
|
@@ -22,14 +25,16 @@ const GroupRouteModal = ({ mode }) => {
|
|
|
22
25
|
});
|
|
23
26
|
};
|
|
24
27
|
}, [pathname]);
|
|
25
|
-
const onClose = useCallback(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
?.focus
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
const onClose = useCallback(
|
|
29
|
+
(event) => {
|
|
30
|
+
keyboardClose.current =
|
|
31
|
+
event?.key === "Escape" ||
|
|
32
|
+
event?.detail === 0 ||
|
|
33
|
+
event?.currentTarget?.matches?.(":focus-visible");
|
|
34
|
+
navigate(GROUPS);
|
|
35
|
+
},
|
|
36
|
+
[navigate],
|
|
37
|
+
);
|
|
33
38
|
return mode === "info" ? (
|
|
34
39
|
<GroupInfoModal key={id} onClose={onClose} />
|
|
35
40
|
) : (
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { StrictMode } from "react";
|
|
2
|
+
import { screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
1
4
|
import { render, waitForLoad } from "@truedat/test/render";
|
|
2
5
|
import { GroupCards } from "../GroupCards";
|
|
3
6
|
|
|
@@ -15,3 +18,94 @@ describe("<GroupCards />", () => {
|
|
|
15
18
|
expect(rendered.container).toMatchSnapshot();
|
|
16
19
|
});
|
|
17
20
|
});
|
|
21
|
+
|
|
22
|
+
const searchableGroups = [
|
|
23
|
+
{
|
|
24
|
+
id: 1,
|
|
25
|
+
name: "finance",
|
|
26
|
+
alias: "Finanzas",
|
|
27
|
+
description: "Gestión de presupuestos",
|
|
28
|
+
users: [{ user_name: "jgarcia", full_name: "José García" }],
|
|
29
|
+
},
|
|
30
|
+
{ id: 2, name: "quality", users: [{ user_name: "jgarcia" }] },
|
|
31
|
+
{ id: 3, name: "empty", users: [] },
|
|
32
|
+
{ id: 4, name: "other" },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
it.each([
|
|
36
|
+
["FINANCE", 1],
|
|
37
|
+
["finanzas", 1],
|
|
38
|
+
["gestion", 1],
|
|
39
|
+
[" JGARC ", 2],
|
|
40
|
+
["jose garcia", 1],
|
|
41
|
+
[" ", 4],
|
|
42
|
+
])("matches groups and their members for %s", async (search, count) => {
|
|
43
|
+
const user = userEvent.setup({ delay: null });
|
|
44
|
+
render(<GroupCards groups={searchableGroups} />);
|
|
45
|
+
await user.type(screen.getByRole("textbox"), search);
|
|
46
|
+
expect(screen.getAllByRole("article")).toHaveLength(count);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("shows no results and restores all groups when the search is cleared", async () => {
|
|
50
|
+
const user = userEvent.setup({ delay: null });
|
|
51
|
+
render(<GroupCards groups={searchableGroups} />);
|
|
52
|
+
const search = screen.getByRole("textbox");
|
|
53
|
+
await user.type(search, "unmatched");
|
|
54
|
+
expect(screen.queryAllByRole("article")).toHaveLength(0);
|
|
55
|
+
expect(screen.getByRole("status")).toBeInTheDocument();
|
|
56
|
+
await user.clear(search);
|
|
57
|
+
expect(screen.getAllByRole("article")).toHaveLength(4);
|
|
58
|
+
expect(screen.queryByRole("status")).not.toBeInTheDocument();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("group search introduction", () => {
|
|
62
|
+
const key = "truedat.groups.search-hint.dismissed";
|
|
63
|
+
const hint = "also";
|
|
64
|
+
const options = { state: { authentication: { user_name: "alice" } } };
|
|
65
|
+
beforeEach(() => localStorage.clear());
|
|
66
|
+
afterEach(() => jest.restoreAllMocks());
|
|
67
|
+
|
|
68
|
+
it("keeps the hint visible until dismissed and remembers the dismissal", async () => {
|
|
69
|
+
const user = userEvent.setup({ delay: null });
|
|
70
|
+
const rendered = render(
|
|
71
|
+
<StrictMode>
|
|
72
|
+
<GroupCards />
|
|
73
|
+
</StrictMode>,
|
|
74
|
+
options,
|
|
75
|
+
);
|
|
76
|
+
expect(screen.getByText(hint).tagName).toBe("EM");
|
|
77
|
+
expect(screen.getByText(hint).parentElement.tagName).toBe("STRONG");
|
|
78
|
+
expect(localStorage.getItem(key)).toBeNull();
|
|
79
|
+
await user.click(screen.getByRole("textbox"));
|
|
80
|
+
expect(screen.getByText(hint)).toBeInTheDocument();
|
|
81
|
+
await user.click(screen.getByRole("button", { name: "Close" }));
|
|
82
|
+
expect(screen.queryByText(hint)).not.toBeInTheDocument();
|
|
83
|
+
expect(localStorage.getItem(key)).toBe("true");
|
|
84
|
+
rendered.rerender(
|
|
85
|
+
<StrictMode>
|
|
86
|
+
<GroupCards groups={searchableGroups} />
|
|
87
|
+
</StrictMode>,
|
|
88
|
+
);
|
|
89
|
+
rendered.unmount();
|
|
90
|
+
render(<GroupCards />, options);
|
|
91
|
+
expect(screen.queryByText(hint)).not.toBeInTheDocument();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("remembers dismissal across users in the same browser", () => {
|
|
95
|
+
localStorage.setItem(key, "true");
|
|
96
|
+
render(<GroupCards />, { state: { authentication: { user_name: "bob" } } });
|
|
97
|
+
expect(screen.queryByText(hint)).not.toBeInTheDocument();
|
|
98
|
+
expect(localStorage.length).toBe(1);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("keeps search usable when storage is unavailable", async () => {
|
|
102
|
+
jest.spyOn(Storage.prototype, "getItem").mockImplementation(() => {
|
|
103
|
+
throw new Error("unavailable");
|
|
104
|
+
});
|
|
105
|
+
const user = userEvent.setup({ delay: null });
|
|
106
|
+
render(<GroupCards groups={searchableGroups} />, options);
|
|
107
|
+
expect(screen.getByText(hint)).toBeInTheDocument();
|
|
108
|
+
await user.type(screen.getByRole("textbox"), "finance");
|
|
109
|
+
expect(screen.getAllByRole("article")).toHaveLength(1);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -78,7 +78,7 @@ describe("<GroupForm />", () => {
|
|
|
78
78
|
"Team Alpha",
|
|
79
79
|
);
|
|
80
80
|
await user.type(
|
|
81
|
-
rendered.getByPlaceholderText(/
|
|
81
|
+
rendered.getByPlaceholderText(/Group description/i),
|
|
82
82
|
"Alpha description",
|
|
83
83
|
);
|
|
84
84
|
await user.type(
|
|
@@ -94,7 +94,7 @@ describe("<GroupForm />", () => {
|
|
|
94
94
|
rendered.getByPlaceholderText(/group.form.name.placeholder/i),
|
|
95
95
|
).toHaveValue("Team Alpha"),
|
|
96
96
|
);
|
|
97
|
-
expect(rendered.getByPlaceholderText(/
|
|
97
|
+
expect(rendered.getByPlaceholderText(/Group description/i)).toHaveValue(
|
|
98
98
|
"Alpha description",
|
|
99
99
|
);
|
|
100
100
|
expect(
|