@truedat/auth 8.14.3 → 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
|
@@ -3,53 +3,293 @@ import { Fragment, useState } from "react";
|
|
|
3
3
|
import PropTypes from "prop-types";
|
|
4
4
|
import { connect } from "react-redux";
|
|
5
5
|
import { Link } from "react-router";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
Accordion,
|
|
8
|
+
Button,
|
|
9
|
+
Card,
|
|
10
|
+
Input,
|
|
11
|
+
Header,
|
|
12
|
+
Icon,
|
|
13
|
+
Label,
|
|
14
|
+
Message,
|
|
15
|
+
} from "semantic-ui-react";
|
|
7
16
|
import { FormattedMessage, useIntl } from "react-intl";
|
|
8
|
-
import
|
|
17
|
+
import SearchContext from "@truedat/core/search/SearchContext";
|
|
18
|
+
import SearchFilters from "@truedat/core/search/SearchFilters";
|
|
19
|
+
import FilterDropdown from "@truedat/core/components/FilterDropdown";
|
|
9
20
|
import { USERS_NEW } from "@truedat/core/routes";
|
|
10
21
|
import { filterUsers } from "../selectors/filterUsers";
|
|
11
22
|
import UserCard from "./UserCard";
|
|
23
|
+
import UsersTable from "./UsersTable";
|
|
24
|
+
import { userTypeOptions } from "./UserTypeSelector";
|
|
12
25
|
|
|
13
|
-
|
|
26
|
+
const ROLE_ORDER = ["admin", "user", "agent", "service"];
|
|
27
|
+
const rolePosition = (role = "") => {
|
|
28
|
+
const position = ROLE_ORDER.indexOf(_.toLower(role));
|
|
29
|
+
return position === -1 ? ROLE_ORDER.length : position;
|
|
30
|
+
};
|
|
31
|
+
const userSortKey = ({ full_name = "", user_name = "" }) =>
|
|
32
|
+
`${_.toLower(full_name)}|${_.toLower(user_name)}`;
|
|
33
|
+
const roleSort = ([roleA], [roleB]) => {
|
|
34
|
+
const posA = rolePosition(roleA);
|
|
35
|
+
const posB = rolePosition(roleB);
|
|
36
|
+
if (posA === posB && posA === ROLE_ORDER.length) {
|
|
37
|
+
return _.toLower(roleA).localeCompare(_.toLower(roleB));
|
|
38
|
+
}
|
|
39
|
+
return posA - posB;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const UserCards = ({ users: allUsers }) => {
|
|
14
43
|
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
|
+
const [sort, setSort] = useState({ column: "name", ascending: true });
|
|
49
|
+
const [view, setView] = useState(() => {
|
|
50
|
+
try {
|
|
51
|
+
return localStorage.getItem("truedat.users.view") === "table"
|
|
52
|
+
? "table"
|
|
53
|
+
: "cards";
|
|
54
|
+
} catch {
|
|
55
|
+
return "cards";
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
const changeView = (value) => {
|
|
59
|
+
setView(value);
|
|
60
|
+
try {
|
|
61
|
+
localStorage.setItem("truedat.users.view", value);
|
|
62
|
+
} catch {
|
|
63
|
+
/* Storage may be unavailable. The selection remains local. */
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const [openRoles, setOpenRoles] = useState({});
|
|
15
67
|
const { formatMessage } = useIntl();
|
|
16
|
-
const
|
|
68
|
+
const toggleRole = (role) => {
|
|
69
|
+
setOpenRoles((prev) => ({ ...prev, [role]: !(prev[role] ?? true) }));
|
|
70
|
+
};
|
|
71
|
+
const groupOptions = _.uniqBy("value")(
|
|
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
|
+
};
|
|
99
|
+
const users = _.flow(
|
|
100
|
+
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
|
+
_.sortBy(userSortKey),
|
|
110
|
+
)(allUsers || []);
|
|
111
|
+
const hasFilters = Boolean(filter.trim() || types.length || groups.length);
|
|
17
112
|
const usersByRole = _.flow(
|
|
18
|
-
_.sortBy("role"),
|
|
19
113
|
_.groupBy("role"),
|
|
20
114
|
_.toPairs,
|
|
21
|
-
)(users);
|
|
115
|
+
)(users).sort(roleSort);
|
|
22
116
|
|
|
23
117
|
return (
|
|
24
118
|
<>
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
119
|
+
<div className="users-toolbar">
|
|
120
|
+
<Input
|
|
121
|
+
iconPosition="left"
|
|
122
|
+
action
|
|
123
|
+
value={filter}
|
|
124
|
+
onChange={(_event, { value }) => setFilter(value)}
|
|
125
|
+
placeholder={formatMessage({ id: "user.search.placeholder" })}
|
|
126
|
+
>
|
|
127
|
+
<Icon name="search" aria-hidden="true" />
|
|
128
|
+
<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
|
+
</Input>
|
|
150
|
+
<div className="users-toolbar-actions">
|
|
151
|
+
<div
|
|
152
|
+
className="users-view-buttons"
|
|
153
|
+
role="group"
|
|
154
|
+
aria-label={formatMessage({ id: "users.view.label" })}
|
|
155
|
+
>
|
|
156
|
+
{["cards", "table"].map((mode) => (
|
|
157
|
+
<Button
|
|
158
|
+
key={mode}
|
|
159
|
+
type="button"
|
|
160
|
+
toggle
|
|
161
|
+
active={view === mode}
|
|
162
|
+
aria-pressed={view === mode}
|
|
163
|
+
onClick={() => changeView(mode)}
|
|
164
|
+
icon={mode === "cards" ? "grid layout" : "list"}
|
|
165
|
+
content={formatMessage({ id: `users.view.${mode}` })}
|
|
166
|
+
/>
|
|
48
167
|
))}
|
|
49
|
-
</
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
168
|
+
</div>
|
|
169
|
+
<Button
|
|
170
|
+
primary
|
|
171
|
+
content={formatMessage({ id: "users.actions.create" })}
|
|
172
|
+
icon="add user"
|
|
173
|
+
as={Link}
|
|
174
|
+
to={USERS_NEW}
|
|
175
|
+
/>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
<div className="selectedFilters users-filters">
|
|
179
|
+
{selectedFilters.length > 0 && (
|
|
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) && (
|
|
206
|
+
<a
|
|
207
|
+
className="resetFilters"
|
|
208
|
+
role="button"
|
|
209
|
+
tabIndex={0}
|
|
210
|
+
onKeyDown={(event) => {
|
|
211
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
212
|
+
event.preventDefault();
|
|
213
|
+
event.currentTarget.click();
|
|
214
|
+
}
|
|
215
|
+
}}
|
|
216
|
+
onClick={resetFilters}
|
|
217
|
+
>
|
|
218
|
+
<FormattedMessage id="users.filter.clear" />
|
|
219
|
+
</a>
|
|
220
|
+
)}
|
|
221
|
+
</div>
|
|
222
|
+
<div className="users-results" role="status">
|
|
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>
|
|
238
|
+
{view === "table" ? (
|
|
239
|
+
<UsersTable users={users} sort={sort} onSort={setSort} />
|
|
240
|
+
) : (
|
|
241
|
+
<Accordion fluid styled className="users-role-accordion">
|
|
242
|
+
{usersByRole.map(([role, users]) => {
|
|
243
|
+
const active = openRoles[role] ?? true;
|
|
244
|
+
const panelId = `users-role-panel-${role}`;
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
<Fragment key={role}>
|
|
248
|
+
<Accordion.Title
|
|
249
|
+
active={active}
|
|
250
|
+
className={`users-role-title users-role-title--${role}`}
|
|
251
|
+
>
|
|
252
|
+
<button
|
|
253
|
+
type="button"
|
|
254
|
+
className="users-role-toggle"
|
|
255
|
+
aria-expanded={active}
|
|
256
|
+
aria-controls={panelId}
|
|
257
|
+
onClick={() => toggleRole(role)}
|
|
258
|
+
>
|
|
259
|
+
<span className="users-role-title-content">
|
|
260
|
+
<Icon
|
|
261
|
+
className="users-role-chevron"
|
|
262
|
+
name={active ? "angle up" : "angle down"}
|
|
263
|
+
aria-hidden="true"
|
|
264
|
+
/>
|
|
265
|
+
<Header as="h3" className="users-role-title-header">
|
|
266
|
+
<Header.Content>
|
|
267
|
+
{formatMessage({ id: `user.type.${role}` })}
|
|
268
|
+
</Header.Content>
|
|
269
|
+
</Header>
|
|
270
|
+
<Label circular className="users-role-count">
|
|
271
|
+
{users.length}
|
|
272
|
+
</Label>
|
|
273
|
+
</span>
|
|
274
|
+
</button>
|
|
275
|
+
</Accordion.Title>
|
|
276
|
+
<Accordion.Content
|
|
277
|
+
id={panelId}
|
|
278
|
+
active={active}
|
|
279
|
+
className="users-role-content"
|
|
280
|
+
>
|
|
281
|
+
<Card.Group className="users-role-card-group">
|
|
282
|
+
{users.map((user, i) => (
|
|
283
|
+
<UserCard key={user.id || i} user={user} />
|
|
284
|
+
))}
|
|
285
|
+
</Card.Group>
|
|
286
|
+
</Accordion.Content>
|
|
287
|
+
</Fragment>
|
|
288
|
+
);
|
|
289
|
+
})}
|
|
290
|
+
</Accordion>
|
|
291
|
+
)}
|
|
292
|
+
{_.isEmpty(users) ? (
|
|
53
293
|
<Message
|
|
54
294
|
className="users-message"
|
|
55
295
|
header={
|
|
@@ -66,13 +306,11 @@ export const UserCards = ({ users: allUsers, showFullName }) => {
|
|
|
66
306
|
|
|
67
307
|
UserCards.propTypes = {
|
|
68
308
|
users: PropTypes.array,
|
|
69
|
-
showFullName: PropTypes.bool,
|
|
70
309
|
};
|
|
71
310
|
|
|
72
311
|
export const mapStateToProps = (state) => ({
|
|
73
312
|
users: state.users,
|
|
74
313
|
UserCard: state.userCard,
|
|
75
|
-
showFullName: state.userCardFullName,
|
|
76
314
|
});
|
|
77
315
|
|
|
78
316
|
export default connect(mapStateToProps)(UserCards);
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import PropTypes from "prop-types";
|
|
2
|
+
import { Form } from "semantic-ui-react";
|
|
3
|
+
import { useIntl } from "react-intl";
|
|
4
|
+
import { Controller } from "react-hook-form";
|
|
5
|
+
import UserTypeSelector from "./UserTypeSelector";
|
|
6
|
+
|
|
7
|
+
const isEmail = (value) => !value || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
|
8
|
+
|
|
9
|
+
const groupName = (group) => (typeof group === "string" ? group : group.name);
|
|
10
|
+
|
|
11
|
+
const buildGroupOptions = (groups = [], selected = []) => {
|
|
12
|
+
const fromGroups = groups.map((g, i) => ({
|
|
13
|
+
key: `g_${i}`,
|
|
14
|
+
text: typeof g === "string" ? g : g.alias || g.name || "",
|
|
15
|
+
value: typeof g === "string" ? g : g.name || "",
|
|
16
|
+
}));
|
|
17
|
+
const existing = groups.map(groupName);
|
|
18
|
+
const fromSelected = (selected || [])
|
|
19
|
+
.filter((v) => typeof v === "string" && !existing.includes(v))
|
|
20
|
+
.filter((v, i, a) => a.indexOf(v) === i)
|
|
21
|
+
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
|
|
22
|
+
.map((text, i) => ({ key: `s_${i}`, text, value: text }));
|
|
23
|
+
return [...fromGroups, ...fromSelected];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const TextField = ({
|
|
27
|
+
control,
|
|
28
|
+
errors,
|
|
29
|
+
name,
|
|
30
|
+
rules,
|
|
31
|
+
disabled,
|
|
32
|
+
icon,
|
|
33
|
+
required,
|
|
34
|
+
}) => {
|
|
35
|
+
const { formatMessage } = useIntl();
|
|
36
|
+
return (
|
|
37
|
+
<Controller
|
|
38
|
+
control={control}
|
|
39
|
+
name={name}
|
|
40
|
+
rules={rules}
|
|
41
|
+
render={({ field: { onBlur, onChange, value, ref } }) => (
|
|
42
|
+
<Form.Input
|
|
43
|
+
name={name}
|
|
44
|
+
input={{ ref }}
|
|
45
|
+
disabled={disabled}
|
|
46
|
+
id={name}
|
|
47
|
+
autoComplete="off"
|
|
48
|
+
{...(icon ? { icon, iconPosition: "left" } : {})}
|
|
49
|
+
error={errors[name]?.message}
|
|
50
|
+
label={{
|
|
51
|
+
children: formatMessage({ id: `user.form.${name}` }),
|
|
52
|
+
htmlFor: name,
|
|
53
|
+
}}
|
|
54
|
+
onBlur={onBlur}
|
|
55
|
+
onChange={(_e, { value: v }) => onChange(v)}
|
|
56
|
+
placeholder={formatMessage({ id: `user.form.placeholder.${name}` })}
|
|
57
|
+
value={value}
|
|
58
|
+
required={required}
|
|
59
|
+
/>
|
|
60
|
+
)}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
TextField.propTypes = {
|
|
66
|
+
control: PropTypes.object.isRequired,
|
|
67
|
+
errors: PropTypes.object.isRequired,
|
|
68
|
+
name: PropTypes.string.isRequired,
|
|
69
|
+
rules: PropTypes.object,
|
|
70
|
+
disabled: PropTypes.bool,
|
|
71
|
+
icon: PropTypes.string,
|
|
72
|
+
required: PropTypes.bool,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const Section = ({ barClass, labelId, children }) => {
|
|
76
|
+
const { formatMessage } = useIntl();
|
|
77
|
+
return (
|
|
78
|
+
<>
|
|
79
|
+
<div className="um-section-header">
|
|
80
|
+
<div className={`um-section-bar um-section-bar--${barClass}`} />
|
|
81
|
+
<span className="um-section-label">
|
|
82
|
+
{formatMessage({ id: labelId })}
|
|
83
|
+
</span>
|
|
84
|
+
</div>
|
|
85
|
+
{children}
|
|
86
|
+
</>
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
Section.propTypes = {
|
|
91
|
+
barClass: PropTypes.string.isRequired,
|
|
92
|
+
labelId: PropTypes.string.isRequired,
|
|
93
|
+
children: PropTypes.node,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Personal, type and account fields shared by the create and edit user modals.
|
|
97
|
+
// Both modals validate and focus the first invalid field the same way.
|
|
98
|
+
export const UserFormFields = ({ control, errors, isSubmitting, groups }) => {
|
|
99
|
+
const { formatMessage } = useIntl();
|
|
100
|
+
const requiredMessage = (field) =>
|
|
101
|
+
formatMessage(
|
|
102
|
+
{ id: "form.validation.required" },
|
|
103
|
+
{ prop: formatMessage({ id: `user.form.${field}` }) },
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<>
|
|
108
|
+
<div className="um-form-top um-section">
|
|
109
|
+
<div className="um-section">
|
|
110
|
+
<Section barClass="personal" labelId="user.section.personal">
|
|
111
|
+
<div className="um-personal-fields">
|
|
112
|
+
<TextField
|
|
113
|
+
control={control}
|
|
114
|
+
errors={errors}
|
|
115
|
+
name="full_name"
|
|
116
|
+
rules={{ required: requiredMessage("full_name") }}
|
|
117
|
+
disabled={isSubmitting}
|
|
118
|
+
icon="user"
|
|
119
|
+
required
|
|
120
|
+
/>
|
|
121
|
+
<TextField
|
|
122
|
+
control={control}
|
|
123
|
+
errors={errors}
|
|
124
|
+
name="email"
|
|
125
|
+
rules={{
|
|
126
|
+
validate: (v) =>
|
|
127
|
+
isEmail(v) ||
|
|
128
|
+
formatMessage({ id: "form.validation.email.invalid" }),
|
|
129
|
+
}}
|
|
130
|
+
disabled={isSubmitting}
|
|
131
|
+
icon="at"
|
|
132
|
+
/>
|
|
133
|
+
</div>
|
|
134
|
+
</Section>
|
|
135
|
+
</div>
|
|
136
|
+
<Controller
|
|
137
|
+
control={control}
|
|
138
|
+
name="role"
|
|
139
|
+
render={({ field: { onBlur, onChange, value } }) => (
|
|
140
|
+
<UserTypeSelector
|
|
141
|
+
disabled={isSubmitting}
|
|
142
|
+
id="role"
|
|
143
|
+
label={formatMessage({ id: "user.form.role" })}
|
|
144
|
+
onBlur={onBlur}
|
|
145
|
+
onChange={onChange}
|
|
146
|
+
value={value || "user"}
|
|
147
|
+
required
|
|
148
|
+
/>
|
|
149
|
+
)}
|
|
150
|
+
/>
|
|
151
|
+
</div>
|
|
152
|
+
<div className="um-section">
|
|
153
|
+
<Section barClass="account" labelId="user.section.account">
|
|
154
|
+
<div className="um-form-grid">
|
|
155
|
+
<TextField
|
|
156
|
+
control={control}
|
|
157
|
+
errors={errors}
|
|
158
|
+
name="user_name"
|
|
159
|
+
rules={{ required: requiredMessage("user_name") }}
|
|
160
|
+
disabled={isSubmitting}
|
|
161
|
+
icon="user circle"
|
|
162
|
+
required
|
|
163
|
+
/>
|
|
164
|
+
<TextField
|
|
165
|
+
control={control}
|
|
166
|
+
errors={errors}
|
|
167
|
+
name="external_id"
|
|
168
|
+
disabled={isSubmitting}
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
</Section>
|
|
172
|
+
<Controller
|
|
173
|
+
control={control}
|
|
174
|
+
name="groups"
|
|
175
|
+
render={({ field: { onBlur, onChange, value } }) => (
|
|
176
|
+
<Form.Dropdown
|
|
177
|
+
disabled={isSubmitting}
|
|
178
|
+
id="groups"
|
|
179
|
+
label={formatMessage({ id: "user.form.groups" })}
|
|
180
|
+
search
|
|
181
|
+
selection
|
|
182
|
+
multiple
|
|
183
|
+
allowAdditions
|
|
184
|
+
options={buildGroupOptions(groups, value)}
|
|
185
|
+
onBlur={onBlur}
|
|
186
|
+
onChange={(_e, { value: v }) => onChange(v)}
|
|
187
|
+
placeholder={formatMessage({
|
|
188
|
+
id: "user.form.placeholder.groups",
|
|
189
|
+
})}
|
|
190
|
+
value={value}
|
|
191
|
+
/>
|
|
192
|
+
)}
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
</>
|
|
196
|
+
);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
UserFormFields.propTypes = {
|
|
200
|
+
control: PropTypes.object.isRequired,
|
|
201
|
+
errors: PropTypes.object.isRequired,
|
|
202
|
+
isSubmitting: PropTypes.bool,
|
|
203
|
+
groups: PropTypes.array,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
export default UserFormFields;
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import PropTypes from "prop-types";
|
|
2
2
|
import { Table } from "semantic-ui-react";
|
|
3
3
|
import { DomainTooltip } from "@truedat/bg/taxonomy/components/DomainPath";
|
|
4
|
+
import UserGroupLink from "./UserGroupLink";
|
|
4
5
|
|
|
5
|
-
export const UserGroupAclRow = ({ resource, role, group }) => (
|
|
6
|
+
export const UserGroupAclRow = ({ resource, role, group, groupId }) => (
|
|
6
7
|
<Table.Row>
|
|
8
|
+
<Table.Cell>
|
|
9
|
+
<span className="um-group-chip">
|
|
10
|
+
<UserGroupLink id={groupId} name={group} />
|
|
11
|
+
</span>
|
|
12
|
+
</Table.Cell>
|
|
7
13
|
<Table.Cell content={<DomainTooltip domain={resource} />} />
|
|
8
|
-
<Table.Cell content={group} />
|
|
9
14
|
<Table.Cell content={role} />
|
|
10
15
|
</Table.Row>
|
|
11
16
|
);
|
|
@@ -14,6 +19,7 @@ UserGroupAclRow.propTypes = {
|
|
|
14
19
|
resource: PropTypes.object,
|
|
15
20
|
role: PropTypes.string,
|
|
16
21
|
group: PropTypes.string,
|
|
22
|
+
groupId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
17
23
|
};
|
|
18
24
|
|
|
19
25
|
export default UserGroupAclRow;
|
|
@@ -15,10 +15,10 @@ export const UserGroupAcls = ({ acls }) => {
|
|
|
15
15
|
<Table.Header>
|
|
16
16
|
<Table.Row>
|
|
17
17
|
<Table.HeaderCell
|
|
18
|
-
content={formatMessage({ id: "user.acl.
|
|
18
|
+
content={formatMessage({ id: "user.acl.group" })}
|
|
19
19
|
/>
|
|
20
20
|
<Table.HeaderCell
|
|
21
|
-
content={formatMessage({ id: "user.acl.
|
|
21
|
+
content={formatMessage({ id: "user.acl.domain" })}
|
|
22
22
|
/>
|
|
23
23
|
<Table.HeaderCell
|
|
24
24
|
content={formatMessage({ id: "user.acl.role" })}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import PropTypes from "prop-types";
|
|
2
|
+
import { Icon } from "semantic-ui-react";
|
|
3
|
+
import { Link } from "react-router";
|
|
4
|
+
import { linkTo } from "@truedat/core/routes";
|
|
5
|
+
|
|
6
|
+
const UserGroupLink = ({ id, name }) =>
|
|
7
|
+
id != null ? (
|
|
8
|
+
<Link to={linkTo.GROUP({ id })} target="_blank" rel="noopener noreferrer">
|
|
9
|
+
<Icon name="external alternate" aria-hidden="true" />
|
|
10
|
+
<span>{name}</span>
|
|
11
|
+
</Link>
|
|
12
|
+
) : (
|
|
13
|
+
name
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
UserGroupLink.propTypes = {
|
|
17
|
+
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
18
|
+
name: PropTypes.string,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default UserGroupLink;
|