@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
|
@@ -49,6 +49,7 @@ const GroupsBadge = ({ groups, id }) => {
|
|
|
49
49
|
return (
|
|
50
50
|
<Popup
|
|
51
51
|
className="uc-groups-popover"
|
|
52
|
+
transition={{ duration: 0 }}
|
|
52
53
|
content={
|
|
53
54
|
<div className="uc-groups-popup">
|
|
54
55
|
<div className="uc-groups-popup__header">
|
|
@@ -135,7 +136,7 @@ export const UserCard = ({
|
|
|
135
136
|
className="uc-external-id"
|
|
136
137
|
title={`${formatMessage({
|
|
137
138
|
id: "user.form.external_id",
|
|
138
|
-
defaultMessage: "ID
|
|
139
|
+
defaultMessage: "External ID",
|
|
139
140
|
})}: ${externalId}`}
|
|
140
141
|
>
|
|
141
142
|
{externalId}
|
|
@@ -150,6 +151,7 @@ export const UserCard = ({
|
|
|
150
151
|
className="uc-action-btn"
|
|
151
152
|
to={linkTo.USER_EDIT({ id })}
|
|
152
153
|
aria-label={editUserLabel}
|
|
154
|
+
title={editUserLabel}
|
|
153
155
|
onClick={(e) => e.stopPropagation()}
|
|
154
156
|
>
|
|
155
157
|
<Icon name="pencil alternate" />
|
|
@@ -159,6 +161,7 @@ export const UserCard = ({
|
|
|
159
161
|
className="uc-action-btn uc-action-btn--key"
|
|
160
162
|
to={linkTo.USER_EDIT_PASSWORD({ id })}
|
|
161
163
|
aria-label={editPasswordLabel}
|
|
164
|
+
title={editPasswordLabel}
|
|
162
165
|
onClick={(e) => e.stopPropagation()}
|
|
163
166
|
>
|
|
164
167
|
<Icon name="key" />
|
|
@@ -14,14 +14,10 @@ import {
|
|
|
14
14
|
Message,
|
|
15
15
|
} from "semantic-ui-react";
|
|
16
16
|
import { FormattedMessage, useIntl } from "react-intl";
|
|
17
|
-
import SearchContext from "@truedat/core/search/SearchContext";
|
|
18
|
-
import SearchFilters from "@truedat/core/search/SearchFilters";
|
|
19
|
-
import FilterDropdown from "@truedat/core/components/FilterDropdown";
|
|
20
17
|
import { USERS_NEW } from "@truedat/core/routes";
|
|
21
18
|
import { filterUsers } from "../selectors/filterUsers";
|
|
22
19
|
import UserCard from "./UserCard";
|
|
23
20
|
import UsersTable from "./UsersTable";
|
|
24
|
-
import { userTypeOptions } from "./UserTypeSelector";
|
|
25
21
|
|
|
26
22
|
const ROLE_ORDER = ["admin", "user", "agent", "service"];
|
|
27
23
|
const rolePosition = (role = "") => {
|
|
@@ -41,10 +37,6 @@ const roleSort = ([roleA], [roleB]) => {
|
|
|
41
37
|
|
|
42
38
|
export const UserCards = ({ users: allUsers }) => {
|
|
43
39
|
const [filter, setFilter] = useState("");
|
|
44
|
-
const [selectedFilters, setSelectedFilters] = useState([]);
|
|
45
|
-
const [openFilter, setOpenFilter] = useState(null);
|
|
46
|
-
const [types, setTypes] = useState([]);
|
|
47
|
-
const [groups, setGroups] = useState([]);
|
|
48
40
|
const [sort, setSort] = useState({ column: "name", ascending: true });
|
|
49
41
|
const [view, setView] = useState(() => {
|
|
50
42
|
try {
|
|
@@ -68,102 +60,65 @@ export const UserCards = ({ users: allUsers }) => {
|
|
|
68
60
|
const toggleRole = (role) => {
|
|
69
61
|
setOpenRoles((prev) => ({ ...prev, [role]: !(prev[role] ?? true) }));
|
|
70
62
|
};
|
|
71
|
-
const
|
|
72
|
-
(allUsers || []).flatMap((user) =>
|
|
73
|
-
(user.groups || []).map((group) => ({
|
|
74
|
-
key: typeof group === "string" ? group : group.name,
|
|
75
|
-
value: typeof group === "string" ? group : group.name,
|
|
76
|
-
text: typeof group === "string" ? group : group.alias || group.name,
|
|
77
|
-
})),
|
|
78
|
-
),
|
|
79
|
-
).sort((a, b) => a.text.localeCompare(b.text));
|
|
80
|
-
const filterOptions = {
|
|
81
|
-
"users.type": userTypeOptions.map(({ value, messageId }) => ({
|
|
82
|
-
value,
|
|
83
|
-
text: formatMessage({ id: messageId }),
|
|
84
|
-
})),
|
|
85
|
-
"users.groups": groupOptions,
|
|
86
|
-
};
|
|
87
|
-
const resetFilters = () => {
|
|
88
|
-
setFilter("");
|
|
89
|
-
setTypes([]);
|
|
90
|
-
setGroups([]);
|
|
91
|
-
setSelectedFilters([]);
|
|
92
|
-
setOpenFilter(null);
|
|
93
|
-
};
|
|
94
|
-
const removeFilter = ({ filter: name }) => {
|
|
95
|
-
setSelectedFilters((values) => values.filter((value) => value !== name));
|
|
96
|
-
(name === "users.type" ? setTypes : setGroups)([]);
|
|
97
|
-
setOpenFilter(null);
|
|
98
|
-
};
|
|
63
|
+
const resetFilters = () => setFilter("");
|
|
99
64
|
const users = _.flow(
|
|
100
65
|
filterUsers(filter),
|
|
101
|
-
_.filter(
|
|
102
|
-
(user) =>
|
|
103
|
-
(!types.length || types.includes(user.role)) &&
|
|
104
|
-
(!groups.length ||
|
|
105
|
-
(user.groups || []).some((group) =>
|
|
106
|
-
groups.includes(typeof group === "string" ? group : group.name),
|
|
107
|
-
)),
|
|
108
|
-
),
|
|
109
66
|
_.sortBy(userSortKey),
|
|
110
67
|
)(allUsers || []);
|
|
111
|
-
const hasFilters = Boolean(filter.trim()
|
|
68
|
+
const hasFilters = Boolean(filter.trim());
|
|
112
69
|
const usersByRole = _.flow(
|
|
113
70
|
_.groupBy("role"),
|
|
114
71
|
_.toPairs,
|
|
115
72
|
)(users).sort(roleSort);
|
|
116
73
|
|
|
74
|
+
const results = (
|
|
75
|
+
<div className="users-results" role="status">
|
|
76
|
+
<Label>
|
|
77
|
+
{hasFilters && (
|
|
78
|
+
<>
|
|
79
|
+
<FormattedMessage
|
|
80
|
+
id="users.results.filtered"
|
|
81
|
+
values={{ count: users.length }}
|
|
82
|
+
/>{" "}
|
|
83
|
+
</>
|
|
84
|
+
)}
|
|
85
|
+
<FormattedMessage
|
|
86
|
+
id="users.results"
|
|
87
|
+
values={{ count: (allUsers || []).length }}
|
|
88
|
+
/>
|
|
89
|
+
</Label>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
|
|
117
93
|
return (
|
|
118
94
|
<>
|
|
119
95
|
<div className="users-toolbar">
|
|
120
96
|
<Input
|
|
121
97
|
iconPosition="left"
|
|
122
|
-
action
|
|
123
98
|
value={filter}
|
|
124
99
|
onChange={(_event, { value }) => setFilter(value)}
|
|
125
100
|
placeholder={formatMessage({ id: "user.search.placeholder" })}
|
|
126
101
|
>
|
|
127
102
|
<Icon name="search" aria-hidden="true" />
|
|
128
103
|
<input />
|
|
129
|
-
<SearchContext
|
|
130
|
-
value={{
|
|
131
|
-
availableGroupedFilters: [
|
|
132
|
-
[
|
|
133
|
-
"",
|
|
134
|
-
Object.keys(filterOptions).filter(
|
|
135
|
-
(name) => !selectedFilters.includes(name),
|
|
136
|
-
),
|
|
137
|
-
],
|
|
138
|
-
],
|
|
139
|
-
addFilter: ({ filter: name }) => {
|
|
140
|
-
setSelectedFilters((values) => [...values, name]);
|
|
141
|
-
setOpenFilter(name);
|
|
142
|
-
},
|
|
143
|
-
resetFilters,
|
|
144
|
-
loadingFilters: false,
|
|
145
|
-
}}
|
|
146
|
-
>
|
|
147
|
-
<SearchFilters />
|
|
148
|
-
</SearchContext>
|
|
149
104
|
</Input>
|
|
150
105
|
<div className="users-toolbar-actions">
|
|
151
106
|
<div
|
|
152
|
-
className="users-view-buttons"
|
|
107
|
+
className="ui buttons users-view-buttons"
|
|
153
108
|
role="group"
|
|
154
109
|
aria-label={formatMessage({ id: "users.view.label" })}
|
|
155
110
|
>
|
|
156
111
|
{["cards", "table"].map((mode) => (
|
|
157
|
-
<
|
|
112
|
+
<button
|
|
158
113
|
key={mode}
|
|
159
114
|
type="button"
|
|
160
|
-
|
|
161
|
-
active={view === mode}
|
|
115
|
+
className={`ui ${view === mode ? "active primary " : ""}button`}
|
|
162
116
|
aria-pressed={view === mode}
|
|
163
117
|
onClick={() => changeView(mode)}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
118
|
+
>
|
|
119
|
+
<Icon name={mode === "cards" ? "grid layout" : "list"} />
|
|
120
|
+
{formatMessage({ id: `users.view.${mode}` })}
|
|
121
|
+
</button>
|
|
167
122
|
))}
|
|
168
123
|
</div>
|
|
169
124
|
<Button
|
|
@@ -175,34 +130,8 @@ export const UserCards = ({ users: allUsers }) => {
|
|
|
175
130
|
/>
|
|
176
131
|
</div>
|
|
177
132
|
</div>
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
<div className="appliedFilters">
|
|
181
|
-
<FormattedMessage id="search.applied_filters" />
|
|
182
|
-
</div>
|
|
183
|
-
)}
|
|
184
|
-
{selectedFilters.map((name) => (
|
|
185
|
-
<FilterDropdown
|
|
186
|
-
key={name}
|
|
187
|
-
filter={name}
|
|
188
|
-
loading={false}
|
|
189
|
-
options={openFilter === name ? filterOptions[name] : null}
|
|
190
|
-
activeValues={name === "users.type" ? types : groups}
|
|
191
|
-
openFilter={() => setOpenFilter(name)}
|
|
192
|
-
closeFilter={() =>
|
|
193
|
-
setOpenFilter((current) => (current === name ? null : current))
|
|
194
|
-
}
|
|
195
|
-
removeFilter={removeFilter}
|
|
196
|
-
toggleFilterValue={({ value }) =>
|
|
197
|
-
(name === "users.type" ? setTypes : setGroups)((values) =>
|
|
198
|
-
values.includes(value)
|
|
199
|
-
? values.filter((item) => item !== value)
|
|
200
|
-
: [...values, value],
|
|
201
|
-
)
|
|
202
|
-
}
|
|
203
|
-
/>
|
|
204
|
-
))}
|
|
205
|
-
{(hasFilters || selectedFilters.length > 0) && (
|
|
133
|
+
{hasFilters && (
|
|
134
|
+
<div className="selectedFilters users-filters">
|
|
206
135
|
<a
|
|
207
136
|
className="resetFilters"
|
|
208
137
|
role="button"
|
|
@@ -217,29 +146,14 @@ export const UserCards = ({ users: allUsers }) => {
|
|
|
217
146
|
>
|
|
218
147
|
<FormattedMessage id="users.filter.clear" />
|
|
219
148
|
</a>
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
<Label>
|
|
224
|
-
{hasFilters && (
|
|
225
|
-
<>
|
|
226
|
-
<FormattedMessage
|
|
227
|
-
id="users.results.filtered"
|
|
228
|
-
values={{ count: users.length }}
|
|
229
|
-
/>{" "}
|
|
230
|
-
</>
|
|
231
|
-
)}
|
|
232
|
-
<FormattedMessage
|
|
233
|
-
id="users.results"
|
|
234
|
-
values={{ count: (allUsers || []).length }}
|
|
235
|
-
/>
|
|
236
|
-
</Label>
|
|
237
|
-
</div>
|
|
149
|
+
</div>
|
|
150
|
+
)}
|
|
151
|
+
{(view === "table" || usersByRole.length === 0) && results}
|
|
238
152
|
{view === "table" ? (
|
|
239
153
|
<UsersTable users={users} sort={sort} onSort={setSort} />
|
|
240
154
|
) : (
|
|
241
155
|
<Accordion fluid styled className="users-role-accordion">
|
|
242
|
-
{usersByRole.map(([role, users]) => {
|
|
156
|
+
{usersByRole.map(([role, users], index) => {
|
|
243
157
|
const active = openRoles[role] ?? true;
|
|
244
158
|
const panelId = `users-role-panel-${role}`;
|
|
245
159
|
|
|
@@ -272,6 +186,7 @@ export const UserCards = ({ users: allUsers }) => {
|
|
|
272
186
|
</Label>
|
|
273
187
|
</span>
|
|
274
188
|
</button>
|
|
189
|
+
{index === 0 && results}
|
|
275
190
|
</Accordion.Title>
|
|
276
191
|
<Accordion.Content
|
|
277
192
|
id={panelId}
|
|
@@ -8,8 +8,7 @@ import TrueModal from "@truedat/core/components/TrueModal";
|
|
|
8
8
|
import { linkTo } from "@truedat/core/routes";
|
|
9
9
|
import { useGroups } from "../../groups/hooks/useGroups";
|
|
10
10
|
import { getUserAcls, getUserGroupAcls } from "../selectors";
|
|
11
|
-
import {
|
|
12
|
-
import { UserGroupAcls } from "./UserGroupAcls";
|
|
11
|
+
import { UnifiedAcls } from "./UserAcls";
|
|
13
12
|
import UserGroupLink from "./UserGroupLink";
|
|
14
13
|
import { userTypeOptions } from "./UserTypeSelector";
|
|
15
14
|
|
|
@@ -28,7 +27,6 @@ export const UserInfoModal = ({
|
|
|
28
27
|
const { formatMessage } = useIntl();
|
|
29
28
|
const [groupByRole, setGroupByRole] = useState(false);
|
|
30
29
|
const [aclsOpen, setAclsOpen] = useState(true);
|
|
31
|
-
const [groupAclsOpen, setGroupAclsOpen] = useState(true);
|
|
32
30
|
|
|
33
31
|
if (!user?.id) return null;
|
|
34
32
|
|
|
@@ -39,10 +37,10 @@ export const UserInfoModal = ({
|
|
|
39
37
|
(nameParts.length > 1 ? nameParts[nameParts.length - 1][0] : "");
|
|
40
38
|
const userType = userTypeOptions.find(({ value }) => value === user.role);
|
|
41
39
|
const unavailable = formatMessage({ id: "user.info.unavailable" });
|
|
42
|
-
const
|
|
43
|
-
const
|
|
40
|
+
const permissions = [...(acls || []), ...(groupAcls || [])];
|
|
41
|
+
const roleCount = new Set(permissions.map(({ role }) => role)).size;
|
|
42
|
+
const hasAcls = permissions.length > 0;
|
|
44
43
|
const aclsPanelId = `user-acls-panel-${user.id}`;
|
|
45
|
-
const groupAclsPanelId = `user-group-acls-panel-${user.id}`;
|
|
46
44
|
|
|
47
45
|
return (
|
|
48
46
|
<TrueModal
|
|
@@ -61,7 +59,7 @@ export const UserInfoModal = ({
|
|
|
61
59
|
},
|
|
62
60
|
{
|
|
63
61
|
key: "edit",
|
|
64
|
-
content: formatMessage({ id: "
|
|
62
|
+
content: formatMessage({ id: "actions.edit" }),
|
|
65
63
|
primary: true,
|
|
66
64
|
as: Link,
|
|
67
65
|
to: linkTo.USER_EDIT({ id: user.id }),
|
|
@@ -163,21 +161,22 @@ export const UserInfoModal = ({
|
|
|
163
161
|
<Icon name="shield alternate" />
|
|
164
162
|
</span>
|
|
165
163
|
<span className="um-acl-title">
|
|
166
|
-
<FormattedMessage id="user.
|
|
167
|
-
<span className="um-acl-count">
|
|
168
|
-
({new Set(acls.map(({ role }) => role)).size})
|
|
169
|
-
</span>
|
|
164
|
+
<FormattedMessage id="user.acl" />
|
|
165
|
+
<span className="um-acl-count">({roleCount})</span>
|
|
170
166
|
</span>
|
|
171
167
|
<Icon name={aclsOpen ? "chevron up" : "chevron down"} />
|
|
172
168
|
</button>
|
|
173
|
-
|
|
174
|
-
<
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
169
|
+
{roleCount > 1 && (
|
|
170
|
+
<div className="um-acl-grouping">
|
|
171
|
+
<Checkbox
|
|
172
|
+
id={`user-group-by-role-${user.id}`}
|
|
173
|
+
toggle
|
|
174
|
+
label={formatMessage({ id: "user.info.group_by_role" })}
|
|
175
|
+
checked={groupByRole}
|
|
176
|
+
onChange={(_event, { checked }) => setGroupByRole(checked)}
|
|
177
|
+
/>
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
181
180
|
</div>
|
|
182
181
|
{aclsOpen && (
|
|
183
182
|
<div
|
|
@@ -185,13 +184,13 @@ export const UserInfoModal = ({
|
|
|
185
184
|
className="um-acl-panel"
|
|
186
185
|
role="region"
|
|
187
186
|
aria-label={formatMessage({
|
|
188
|
-
id: "user.
|
|
187
|
+
id: "user.acl",
|
|
189
188
|
})}
|
|
190
189
|
tabIndex={0}
|
|
191
190
|
>
|
|
192
|
-
<
|
|
193
|
-
acls={
|
|
194
|
-
groupBy={groupByRole ? "role" : "
|
|
191
|
+
<UnifiedAcls
|
|
192
|
+
acls={permissions}
|
|
193
|
+
groupBy={groupByRole && roleCount > 1 ? "role" : "group"}
|
|
195
194
|
/>
|
|
196
195
|
</div>
|
|
197
196
|
)}
|
|
@@ -202,45 +201,6 @@ export const UserInfoModal = ({
|
|
|
202
201
|
<FormattedMessage id="user.info.no_permissions" />
|
|
203
202
|
</p>
|
|
204
203
|
)}
|
|
205
|
-
|
|
206
|
-
{hasGroupAcls ? (
|
|
207
|
-
<div className="um-acl-section um-acl-section--groups">
|
|
208
|
-
<button
|
|
209
|
-
type="button"
|
|
210
|
-
className="um-acl-header"
|
|
211
|
-
aria-expanded={groupAclsOpen}
|
|
212
|
-
aria-controls={groupAclsPanelId}
|
|
213
|
-
onClick={() => setGroupAclsOpen((open) => !open)}
|
|
214
|
-
>
|
|
215
|
-
<span className="um-acl-icon--groups">
|
|
216
|
-
<Icon name="users" />
|
|
217
|
-
</span>
|
|
218
|
-
<span className="um-acl-title">
|
|
219
|
-
<FormattedMessage id="user.info.group_permissions" />
|
|
220
|
-
<span className="um-acl-count">({groupAcls.length})</span>
|
|
221
|
-
</span>
|
|
222
|
-
<Icon name={groupAclsOpen ? "chevron up" : "chevron down"} />
|
|
223
|
-
</button>
|
|
224
|
-
{groupAclsOpen && (
|
|
225
|
-
<div
|
|
226
|
-
id={groupAclsPanelId}
|
|
227
|
-
className="um-acl-panel"
|
|
228
|
-
role="region"
|
|
229
|
-
aria-label={formatMessage({
|
|
230
|
-
id: "user.info.group_permissions",
|
|
231
|
-
})}
|
|
232
|
-
tabIndex={0}
|
|
233
|
-
>
|
|
234
|
-
<UserGroupAcls acls={groupAcls} />
|
|
235
|
-
</div>
|
|
236
|
-
)}
|
|
237
|
-
</div>
|
|
238
|
-
) : (
|
|
239
|
-
<p className="um-empty">
|
|
240
|
-
<Icon name="users" aria-hidden="true" />
|
|
241
|
-
<FormattedMessage id="user.info.no_group_permissions" />
|
|
242
|
-
</p>
|
|
243
|
-
)}
|
|
244
204
|
</div>
|
|
245
205
|
</TrueModal>
|
|
246
206
|
);
|
|
@@ -37,7 +37,7 @@ describe("<EditUser />", () => {
|
|
|
37
37
|
const rendered = render(routes, { ...renderOptions, dispatch });
|
|
38
38
|
expect(rendered.getByRole("dialog")).toHaveClass("user-modal--edit");
|
|
39
39
|
expect(document.getElementById("full_name")).toHaveValue(account.full_name);
|
|
40
|
-
fireEvent.click(rendered.getByRole("button", { name: "actions.
|
|
40
|
+
fireEvent.click(rendered.getByRole("button", { name: "actions.close" }));
|
|
41
41
|
expect(await rendered.findByText("User list")).toBeVisible();
|
|
42
42
|
expect(dispatch).not.toHaveBeenCalledWith(
|
|
43
43
|
expect.objectContaining({ type: updateUser.TRIGGER }),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { screen, waitFor, within } from "@testing-library/react";
|
|
2
2
|
import { render } from "@truedat/test/render";
|
|
3
|
-
import UserAcls, { UserAcls as UserAclsTable } from "../UserAcls";
|
|
3
|
+
import UserAcls, { UserAcls as UserAclsTable, UnifiedAcls } from "../UserAcls";
|
|
4
4
|
|
|
5
5
|
const renderOpts = {
|
|
6
6
|
messages: {
|
|
@@ -53,3 +53,42 @@ it("groups roles by domain identity and deduplicates repeated assignments", () =
|
|
|
53
53
|
within(screen.getAllByRole("row")[2]).getAllByRole("listitem"),
|
|
54
54
|
).toHaveLength(2);
|
|
55
55
|
});
|
|
56
|
+
|
|
57
|
+
it.each(["group", "role"])(
|
|
58
|
+
"keeps each permission's origin when grouping unified permissions by %s",
|
|
59
|
+
(groupBy) => {
|
|
60
|
+
const resource = { id: 1, name: "Finance", type: "domain" };
|
|
61
|
+
const acls = [
|
|
62
|
+
{ resource, role: "Reader" },
|
|
63
|
+
{ resource, role: "Reader", group: "Team", groupId: 7 },
|
|
64
|
+
{ resource, role: "Reader", group: "Team", groupId: 8 },
|
|
65
|
+
{ resource, role: "Editor", group: "Team", groupId: 7 },
|
|
66
|
+
];
|
|
67
|
+
render(<UnifiedAcls acls={acls} groupBy={groupBy} />);
|
|
68
|
+
const rows = screen.getAllByRole("row").slice(1);
|
|
69
|
+
expect(rows).toHaveLength(4);
|
|
70
|
+
expect(
|
|
71
|
+
screen
|
|
72
|
+
.getAllByRole("link", { name: "Team" })
|
|
73
|
+
.map((link) => link.getAttribute("href")),
|
|
74
|
+
).toEqual(
|
|
75
|
+
groupBy === "group"
|
|
76
|
+
? ["/groups/7", "/groups/8"]
|
|
77
|
+
: ["/groups/7", "/groups/7", "/groups/8"],
|
|
78
|
+
);
|
|
79
|
+
const directRow = rows.find((row) =>
|
|
80
|
+
within(row).queryByRole("cell", { name: "" }),
|
|
81
|
+
);
|
|
82
|
+
expect(directRow).toBeDefined();
|
|
83
|
+
if (groupBy === "role") {
|
|
84
|
+
expect(screen.getByRole("cell", { name: "Reader" })).toHaveAttribute(
|
|
85
|
+
"rowspan",
|
|
86
|
+
"3",
|
|
87
|
+
);
|
|
88
|
+
} else {
|
|
89
|
+
expect(
|
|
90
|
+
screen.getAllByRole("link", { name: "Team" })[0].closest("td"),
|
|
91
|
+
).toHaveAttribute("rowspan", "2");
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
);
|
|
@@ -29,7 +29,7 @@ describe("<UserCard />", () => {
|
|
|
29
29
|
);
|
|
30
30
|
expect(rendered.getByText("employee-123")).toHaveAttribute(
|
|
31
31
|
"title",
|
|
32
|
-
"ID
|
|
32
|
+
"External ID: employee-123",
|
|
33
33
|
);
|
|
34
34
|
expect(rendered.getByText(user.user_name)).toBeInTheDocument();
|
|
35
35
|
expect(rendered.getByText(user.email)).toBeInTheDocument();
|
|
@@ -159,51 +159,6 @@ it("works when storage is unavailable and reuses the delete confirmation", async
|
|
|
159
159
|
set.mockRestore();
|
|
160
160
|
});
|
|
161
161
|
|
|
162
|
-
it("combines type and group filters and keeps all group options available", async () => {
|
|
163
|
-
localStorage.clear();
|
|
164
|
-
const user = userEvent.setup({ delay: null });
|
|
165
|
-
render(
|
|
166
|
-
<UserCards
|
|
167
|
-
users={[
|
|
168
|
-
{ id: 1, user_name: "Alpha", role: "user", groups: ["Data"], acls: [] },
|
|
169
|
-
{
|
|
170
|
-
id: 2,
|
|
171
|
-
user_name: "Beta",
|
|
172
|
-
role: "admin",
|
|
173
|
-
groups: [{ name: "quality", alias: "Quality" }],
|
|
174
|
-
},
|
|
175
|
-
{
|
|
176
|
-
id: 3,
|
|
177
|
-
user_name: "Gamma",
|
|
178
|
-
role: "user",
|
|
179
|
-
groups: ["Data"],
|
|
180
|
-
acls: [{ role: {} }],
|
|
181
|
-
},
|
|
182
|
-
]}
|
|
183
|
-
/>,
|
|
184
|
-
);
|
|
185
|
-
await user.click(screen.getByRole("button", { name: "users.view.table" }));
|
|
186
|
-
await user.click(screen.getByText("Filters", { exact: true }));
|
|
187
|
-
await user.click(screen.getByRole("option", { name: "type", exact: true }));
|
|
188
|
-
await user.click(screen.getByRole("option", { name: "user.type.user" }));
|
|
189
|
-
await user.keyboard("{Escape}");
|
|
190
|
-
await user.click(screen.getByText("Filters", { exact: true }));
|
|
191
|
-
await user.click(screen.getByRole("option", { name: "groups", exact: true }));
|
|
192
|
-
expect(screen.getByRole("option", { name: "Quality" })).toBeInTheDocument();
|
|
193
|
-
await user.click(screen.getByRole("option", { name: "Data" }));
|
|
194
|
-
await user.keyboard("{Escape}");
|
|
195
|
-
expect(screen.getAllByRole("row")).toHaveLength(3);
|
|
196
|
-
expect(screen.getAllByText("users.table.unknown")).toHaveLength(1);
|
|
197
|
-
await user.type(
|
|
198
|
-
screen.getByPlaceholderText(/user.search.placeholder/),
|
|
199
|
-
"alpha",
|
|
200
|
-
);
|
|
201
|
-
expect(screen.getAllByRole("row")).toHaveLength(2);
|
|
202
|
-
await user.click(screen.getByRole("button", { name: "users.filter.clear" }));
|
|
203
|
-
expect(screen.getAllByRole("row")).toHaveLength(4);
|
|
204
|
-
localStorage.clear();
|
|
205
|
-
});
|
|
206
|
-
|
|
207
162
|
describe("<UserCards /> role sections", () => {
|
|
208
163
|
beforeEach(() => localStorage.clear());
|
|
209
164
|
const users = [
|
|
@@ -31,9 +31,7 @@ describe("User modal shell", () => {
|
|
|
31
31
|
fireEvent.change(document.getElementById("full_name"), {
|
|
32
32
|
target: { value: "Alice Updated" },
|
|
33
33
|
});
|
|
34
|
-
await keyboard.click(
|
|
35
|
-
screen.getByRole("button", { name: "Eliminar usuario" }),
|
|
36
|
-
);
|
|
34
|
+
await keyboard.click(screen.getByRole("button", { name: "Delete user" }));
|
|
37
35
|
expect(screen.getAllByRole("dialog")).toHaveLength(1);
|
|
38
36
|
expect(
|
|
39
37
|
screen.getByRole("button", { name: "user.delete.submit" }),
|
|
@@ -43,9 +41,7 @@ describe("User modal shell", () => {
|
|
|
43
41
|
screen.getByRole("button", { name: "actions.cancel" }),
|
|
44
42
|
);
|
|
45
43
|
expect(document.getElementById("full_name")).toHaveValue("Alice Updated");
|
|
46
|
-
await keyboard.click(
|
|
47
|
-
screen.getByRole("button", { name: "Eliminar usuario" }),
|
|
48
|
-
);
|
|
44
|
+
await keyboard.click(screen.getByRole("button", { name: "Delete user" }));
|
|
49
45
|
await keyboard.click(
|
|
50
46
|
screen.getByRole("button", { name: "user.delete.submit" }),
|
|
51
47
|
);
|
|
@@ -445,7 +441,7 @@ describe("User form interactions and content", () => {
|
|
|
445
441
|
screen.queryByRole("heading", { name: /^Grupos/ }),
|
|
446
442
|
).not.toBeInTheDocument();
|
|
447
443
|
expect(screen.getByText("Sin roles asignados")).toBeVisible();
|
|
448
|
-
expect(screen.
|
|
444
|
+
expect(screen.queryByRole("table")).not.toBeInTheDocument();
|
|
449
445
|
});
|
|
450
446
|
|
|
451
447
|
it("validates the edit form like the creation form and focuses the first invalid field", async () => {
|
|
@@ -505,3 +501,54 @@ describe("User form interactions and content", () => {
|
|
|
505
501
|
expect(screen.getByText("¿Confirmas la eliminación?").tagName).toBe("P");
|
|
506
502
|
});
|
|
507
503
|
});
|
|
504
|
+
|
|
505
|
+
describe("Unified user permissions", () => {
|
|
506
|
+
const resource = { id: 1, type: "domain", name: "Finance" };
|
|
507
|
+
const user = { id: 1, full_name: "Alice", role: "user" };
|
|
508
|
+
|
|
509
|
+
it.each([[[]], [[{ resource, role: "Reader" }]]])(
|
|
510
|
+
"hides role grouping for zero or one distinct role across direct and inherited permissions",
|
|
511
|
+
(acls) => {
|
|
512
|
+
render(
|
|
513
|
+
<UserInfoModal
|
|
514
|
+
user={user}
|
|
515
|
+
acls={acls}
|
|
516
|
+
groupAcls={[{ resource, role: "Reader", group: "Team", groupId: 7 }]}
|
|
517
|
+
onClose={jest.fn()}
|
|
518
|
+
/>,
|
|
519
|
+
);
|
|
520
|
+
expect(screen.queryByRole("checkbox")).not.toBeInTheDocument();
|
|
521
|
+
expect(screen.getAllByRole("table")).toHaveLength(1);
|
|
522
|
+
expect(screen.getByRole("link", { name: "Team" })).toHaveAttribute(
|
|
523
|
+
"href",
|
|
524
|
+
"/groups/7",
|
|
525
|
+
);
|
|
526
|
+
},
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
it("switches between group and role grouping in one collapsible table", async () => {
|
|
530
|
+
render(
|
|
531
|
+
<UserInfoModal
|
|
532
|
+
user={user}
|
|
533
|
+
acls={[{ resource, role: "Editor" }]}
|
|
534
|
+
groupAcls={[{ resource, role: "Reader", group: "Team", groupId: 7 }]}
|
|
535
|
+
onClose={jest.fn()}
|
|
536
|
+
/>,
|
|
537
|
+
);
|
|
538
|
+
const keyboard = userEvent.setup({ delay: null });
|
|
539
|
+
const grouping = screen.getByRole("checkbox");
|
|
540
|
+
expect(grouping).not.toBeChecked();
|
|
541
|
+
await keyboard.click(grouping);
|
|
542
|
+
expect(grouping).toBeChecked();
|
|
543
|
+
expect(screen.getAllByRole("table")).toHaveLength(1);
|
|
544
|
+
expect(screen.getByRole("link", { name: "Team" })).toHaveAttribute(
|
|
545
|
+
"href",
|
|
546
|
+
"/groups/7",
|
|
547
|
+
);
|
|
548
|
+
const header = screen.getByRole("button", { expanded: true });
|
|
549
|
+
await keyboard.click(header);
|
|
550
|
+
expect(screen.queryByRole("table")).not.toBeInTheDocument();
|
|
551
|
+
await keyboard.click(header);
|
|
552
|
+
expect(screen.getByRole("table")).toBeVisible();
|
|
553
|
+
});
|
|
554
|
+
});
|
|
@@ -57,6 +57,7 @@ exports[`<UserCard /> matches the latest snapshot 1`] = `
|
|
|
57
57
|
class="uc-action-btn"
|
|
58
58
|
data-discover="true"
|
|
59
59
|
href="/users/1/edit"
|
|
60
|
+
title="users.actions.edit"
|
|
60
61
|
>
|
|
61
62
|
<i
|
|
62
63
|
aria-hidden="true"
|
|
@@ -73,6 +74,7 @@ exports[`<UserCard /> matches the latest snapshot 1`] = `
|
|
|
73
74
|
class="uc-action-btn uc-action-btn--key"
|
|
74
75
|
data-discover="true"
|
|
75
76
|
href="/users/1/password"
|
|
77
|
+
title="user.actions.edit_password"
|
|
76
78
|
>
|
|
77
79
|
<i
|
|
78
80
|
aria-hidden="true"
|