@truedat/auth 8.14.3 → 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/messages/en.js +6 -5
- package/src/messages/es.js +8 -7
- package/src/styles/Groups.less +96 -131
- package/src/styles/UserCard.less +416 -0
- package/src/styles/UserModals.less +831 -0
- package/src/styles/Users.less +313 -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 +117 -6
- package/src/users/components/UserActions.js +99 -34
- package/src/users/components/UserCard.js +171 -22
- package/src/users/components/UserCards.js +188 -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 +228 -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 +62 -2
- package/src/users/components/__tests__/UserCard.spec.js +111 -5
- package/src/users/components/__tests__/UserCards.spec.js +149 -0
- package/src/users/components/__tests__/UserGroupAclRow.spec.js +19 -1
- package/src/users/components/__tests__/UserModals.spec.js +554 -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 +66 -37
- package/src/users/components/__tests__/__snapshots__/UserCards.spec.js.snap +375 -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
|
@@ -1,32 +1,181 @@
|
|
|
1
1
|
import PropTypes from "prop-types";
|
|
2
|
-
import {
|
|
3
|
-
import { Icon, Card } from "semantic-ui-react";
|
|
2
|
+
import { Icon, Card, Label, Popup } from "semantic-ui-react";
|
|
4
3
|
import { Link } from "react-router";
|
|
4
|
+
import { useIntl } from "react-intl";
|
|
5
5
|
import { linkTo } from "@truedat/core/routes";
|
|
6
|
-
import { deleteUser } from "../routines";
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
7
|
+
const getInitials = (fullName, userName) => {
|
|
8
|
+
const normalizedFullName = fullName?.trim() || "";
|
|
9
|
+
|
|
10
|
+
if (normalizedFullName) {
|
|
11
|
+
const parts = normalizedFullName.split(/\s+/);
|
|
12
|
+
return parts.length >= 2
|
|
13
|
+
? (parts[0][0] + parts[parts.length - 1][0]).toUpperCase()
|
|
14
|
+
: parts[0][0].toUpperCase();
|
|
15
|
+
}
|
|
16
|
+
return userName ? userName[0].toUpperCase() : "?";
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const getGroupName = (group) => {
|
|
20
|
+
if (typeof group === "string") {
|
|
21
|
+
return group;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return group?.alias || group?.name || String(group);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const renderGroupsBadgeLabel = (groupsCount, id) => (
|
|
28
|
+
<Label
|
|
29
|
+
as={Link}
|
|
30
|
+
to={linkTo.USER({ id })}
|
|
31
|
+
attached="bottom right"
|
|
32
|
+
className="overlapping groups-badge"
|
|
33
|
+
>
|
|
34
|
+
<Icon name="users" />
|
|
35
|
+
{groupsCount}
|
|
36
|
+
</Label>
|
|
25
37
|
);
|
|
26
38
|
|
|
39
|
+
const GroupsBadge = ({ groups, id }) => {
|
|
40
|
+
const { formatMessage } = useIntl();
|
|
41
|
+
const groupsCount = groups.length;
|
|
42
|
+
|
|
43
|
+
if (groupsCount === 0) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const label = renderGroupsBadgeLabel(groupsCount, id);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Popup
|
|
51
|
+
className="uc-groups-popover"
|
|
52
|
+
transition={{ duration: 0 }}
|
|
53
|
+
content={
|
|
54
|
+
<div className="uc-groups-popup">
|
|
55
|
+
<div className="uc-groups-popup__header">
|
|
56
|
+
<Icon name="users" />
|
|
57
|
+
<span>{formatMessage({ id: "user.form.groups" })}</span>
|
|
58
|
+
</div>
|
|
59
|
+
<ul className="uc-groups-popup__list">
|
|
60
|
+
{groups.map((group) => {
|
|
61
|
+
const groupName = getGroupName(group);
|
|
62
|
+
return (
|
|
63
|
+
<li className="uc-groups-popup__item" key={groupName}>
|
|
64
|
+
<span className="uc-groups-popup__marker">
|
|
65
|
+
{groupName[0]?.toUpperCase()}
|
|
66
|
+
</span>
|
|
67
|
+
<span className="uc-groups-popup__name">{groupName}</span>
|
|
68
|
+
</li>
|
|
69
|
+
);
|
|
70
|
+
})}
|
|
71
|
+
</ul>
|
|
72
|
+
</div>
|
|
73
|
+
}
|
|
74
|
+
position="bottom center"
|
|
75
|
+
trigger={label}
|
|
76
|
+
/>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
GroupsBadge.propTypes = {
|
|
81
|
+
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
82
|
+
groups: PropTypes.array,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const UserCard = ({
|
|
86
|
+
user: { user_name, id, full_name, email, external_id, groups },
|
|
87
|
+
}) => {
|
|
88
|
+
const { formatMessage } = useIntl();
|
|
89
|
+
const userName = user_name?.trim() || "";
|
|
90
|
+
const fullName = full_name?.trim() || "";
|
|
91
|
+
const displayName = fullName || userName;
|
|
92
|
+
const editUserLabel = formatMessage({ id: "users.actions.edit" });
|
|
93
|
+
const editPasswordLabel = formatMessage({
|
|
94
|
+
id: "user.actions.edit_password",
|
|
95
|
+
});
|
|
96
|
+
const usernameDetail = fullName ? userName : "";
|
|
97
|
+
const userEmail = email?.trim() || "";
|
|
98
|
+
const externalId = external_id?.trim() || "";
|
|
99
|
+
const userGroups = groups || [];
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<Card key={id} className="td-card-base td-card--user">
|
|
103
|
+
<Card.Content
|
|
104
|
+
as={Link}
|
|
105
|
+
className="uc-card-link"
|
|
106
|
+
to={linkTo.USER({ id })}
|
|
107
|
+
aria-label={formatMessage(
|
|
108
|
+
{ id: "user.info.open" },
|
|
109
|
+
{ name: displayName },
|
|
110
|
+
)}
|
|
111
|
+
>
|
|
112
|
+
<div className="uc-summary">
|
|
113
|
+
<div className="uc-avatar">{getInitials(full_name, user_name)}</div>
|
|
114
|
+
<div className="uc-name" title={displayName}>
|
|
115
|
+
{displayName}
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div className="uc-meta-grid">
|
|
120
|
+
{usernameDetail && (
|
|
121
|
+
<>
|
|
122
|
+
<Icon name="user" />
|
|
123
|
+
<span className="uc-username">{usernameDetail}</span>
|
|
124
|
+
</>
|
|
125
|
+
)}
|
|
126
|
+
{userEmail && (
|
|
127
|
+
<>
|
|
128
|
+
<Icon name="mail" />
|
|
129
|
+
<span className="uc-email">{userEmail}</span>
|
|
130
|
+
</>
|
|
131
|
+
)}
|
|
132
|
+
{externalId && (
|
|
133
|
+
<>
|
|
134
|
+
<Icon name="id badge" />
|
|
135
|
+
<span
|
|
136
|
+
className="uc-external-id"
|
|
137
|
+
title={`${formatMessage({
|
|
138
|
+
id: "user.form.external_id",
|
|
139
|
+
defaultMessage: "External ID",
|
|
140
|
+
})}: ${externalId}`}
|
|
141
|
+
>
|
|
142
|
+
{externalId}
|
|
143
|
+
</span>
|
|
144
|
+
</>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
</Card.Content>
|
|
148
|
+
|
|
149
|
+
<div className="uc-actions">
|
|
150
|
+
<Link
|
|
151
|
+
className="uc-action-btn"
|
|
152
|
+
to={linkTo.USER_EDIT({ id })}
|
|
153
|
+
aria-label={editUserLabel}
|
|
154
|
+
title={editUserLabel}
|
|
155
|
+
onClick={(e) => e.stopPropagation()}
|
|
156
|
+
>
|
|
157
|
+
<Icon name="pencil alternate" />
|
|
158
|
+
<span className="uc-action-label">{editUserLabel}</span>
|
|
159
|
+
</Link>
|
|
160
|
+
<Link
|
|
161
|
+
className="uc-action-btn uc-action-btn--key"
|
|
162
|
+
to={linkTo.USER_EDIT_PASSWORD({ id })}
|
|
163
|
+
aria-label={editPasswordLabel}
|
|
164
|
+
title={editPasswordLabel}
|
|
165
|
+
onClick={(e) => e.stopPropagation()}
|
|
166
|
+
>
|
|
167
|
+
<Icon name="key" />
|
|
168
|
+
<span className="uc-action-label">{editPasswordLabel}</span>
|
|
169
|
+
</Link>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<GroupsBadge groups={userGroups} id={id} />
|
|
173
|
+
</Card>
|
|
174
|
+
);
|
|
175
|
+
};
|
|
176
|
+
|
|
27
177
|
UserCard.propTypes = {
|
|
28
178
|
user: PropTypes.object,
|
|
29
|
-
showFullName: PropTypes.bool,
|
|
30
179
|
};
|
|
31
180
|
|
|
32
|
-
export default
|
|
181
|
+
export default UserCard;
|
|
@@ -3,53 +3,208 @@ 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 SearchInput from "@truedat/core/components/SearchInput";
|
|
9
17
|
import { USERS_NEW } from "@truedat/core/routes";
|
|
10
18
|
import { filterUsers } from "../selectors/filterUsers";
|
|
11
19
|
import UserCard from "./UserCard";
|
|
20
|
+
import UsersTable from "./UsersTable";
|
|
12
21
|
|
|
13
|
-
|
|
22
|
+
const ROLE_ORDER = ["admin", "user", "agent", "service"];
|
|
23
|
+
const rolePosition = (role = "") => {
|
|
24
|
+
const position = ROLE_ORDER.indexOf(_.toLower(role));
|
|
25
|
+
return position === -1 ? ROLE_ORDER.length : position;
|
|
26
|
+
};
|
|
27
|
+
const userSortKey = ({ full_name = "", user_name = "" }) =>
|
|
28
|
+
`${_.toLower(full_name)}|${_.toLower(user_name)}`;
|
|
29
|
+
const roleSort = ([roleA], [roleB]) => {
|
|
30
|
+
const posA = rolePosition(roleA);
|
|
31
|
+
const posB = rolePosition(roleB);
|
|
32
|
+
if (posA === posB && posA === ROLE_ORDER.length) {
|
|
33
|
+
return _.toLower(roleA).localeCompare(_.toLower(roleB));
|
|
34
|
+
}
|
|
35
|
+
return posA - posB;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const UserCards = ({ users: allUsers }) => {
|
|
14
39
|
const [filter, setFilter] = useState("");
|
|
40
|
+
const [sort, setSort] = useState({ column: "name", ascending: true });
|
|
41
|
+
const [view, setView] = useState(() => {
|
|
42
|
+
try {
|
|
43
|
+
return localStorage.getItem("truedat.users.view") === "table"
|
|
44
|
+
? "table"
|
|
45
|
+
: "cards";
|
|
46
|
+
} catch {
|
|
47
|
+
return "cards";
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
const changeView = (value) => {
|
|
51
|
+
setView(value);
|
|
52
|
+
try {
|
|
53
|
+
localStorage.setItem("truedat.users.view", value);
|
|
54
|
+
} catch {
|
|
55
|
+
/* Storage may be unavailable. The selection remains local. */
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const [openRoles, setOpenRoles] = useState({});
|
|
15
59
|
const { formatMessage } = useIntl();
|
|
16
|
-
const
|
|
60
|
+
const toggleRole = (role) => {
|
|
61
|
+
setOpenRoles((prev) => ({ ...prev, [role]: !(prev[role] ?? true) }));
|
|
62
|
+
};
|
|
63
|
+
const resetFilters = () => setFilter("");
|
|
64
|
+
const users = _.flow(
|
|
65
|
+
filterUsers(filter),
|
|
66
|
+
_.sortBy(userSortKey),
|
|
67
|
+
)(allUsers || []);
|
|
68
|
+
const hasFilters = Boolean(filter.trim());
|
|
17
69
|
const usersByRole = _.flow(
|
|
18
|
-
_.sortBy("role"),
|
|
19
70
|
_.groupBy("role"),
|
|
20
71
|
_.toPairs,
|
|
21
|
-
)(users);
|
|
72
|
+
)(users).sort(roleSort);
|
|
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
|
+
);
|
|
22
92
|
|
|
23
93
|
return (
|
|
24
94
|
<>
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
95
|
+
<div className="users-toolbar">
|
|
96
|
+
<Input
|
|
97
|
+
iconPosition="left"
|
|
98
|
+
value={filter}
|
|
99
|
+
onChange={(_event, { value }) => setFilter(value)}
|
|
100
|
+
placeholder={formatMessage({ id: "user.search.placeholder" })}
|
|
101
|
+
>
|
|
102
|
+
<Icon name="search" aria-hidden="true" />
|
|
103
|
+
<input />
|
|
104
|
+
</Input>
|
|
105
|
+
<div className="users-toolbar-actions">
|
|
106
|
+
<div
|
|
107
|
+
className="ui buttons users-view-buttons"
|
|
108
|
+
role="group"
|
|
109
|
+
aria-label={formatMessage({ id: "users.view.label" })}
|
|
110
|
+
>
|
|
111
|
+
{["cards", "table"].map((mode) => (
|
|
112
|
+
<button
|
|
113
|
+
key={mode}
|
|
114
|
+
type="button"
|
|
115
|
+
className={`ui ${view === mode ? "active primary " : ""}button`}
|
|
116
|
+
aria-pressed={view === mode}
|
|
117
|
+
onClick={() => changeView(mode)}
|
|
118
|
+
>
|
|
119
|
+
<Icon name={mode === "cards" ? "grid layout" : "list"} />
|
|
120
|
+
{formatMessage({ id: `users.view.${mode}` })}
|
|
121
|
+
</button>
|
|
48
122
|
))}
|
|
49
|
-
</
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
123
|
+
</div>
|
|
124
|
+
<Button
|
|
125
|
+
primary
|
|
126
|
+
content={formatMessage({ id: "users.actions.create" })}
|
|
127
|
+
icon="add user"
|
|
128
|
+
as={Link}
|
|
129
|
+
to={USERS_NEW}
|
|
130
|
+
/>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
{hasFilters && (
|
|
134
|
+
<div className="selectedFilters users-filters">
|
|
135
|
+
<a
|
|
136
|
+
className="resetFilters"
|
|
137
|
+
role="button"
|
|
138
|
+
tabIndex={0}
|
|
139
|
+
onKeyDown={(event) => {
|
|
140
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
141
|
+
event.preventDefault();
|
|
142
|
+
event.currentTarget.click();
|
|
143
|
+
}
|
|
144
|
+
}}
|
|
145
|
+
onClick={resetFilters}
|
|
146
|
+
>
|
|
147
|
+
<FormattedMessage id="users.filter.clear" />
|
|
148
|
+
</a>
|
|
149
|
+
</div>
|
|
150
|
+
)}
|
|
151
|
+
{(view === "table" || usersByRole.length === 0) && results}
|
|
152
|
+
{view === "table" ? (
|
|
153
|
+
<UsersTable users={users} sort={sort} onSort={setSort} />
|
|
154
|
+
) : (
|
|
155
|
+
<Accordion fluid styled className="users-role-accordion">
|
|
156
|
+
{usersByRole.map(([role, users], index) => {
|
|
157
|
+
const active = openRoles[role] ?? true;
|
|
158
|
+
const panelId = `users-role-panel-${role}`;
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<Fragment key={role}>
|
|
162
|
+
<Accordion.Title
|
|
163
|
+
active={active}
|
|
164
|
+
className={`users-role-title users-role-title--${role}`}
|
|
165
|
+
>
|
|
166
|
+
<button
|
|
167
|
+
type="button"
|
|
168
|
+
className="users-role-toggle"
|
|
169
|
+
aria-expanded={active}
|
|
170
|
+
aria-controls={panelId}
|
|
171
|
+
onClick={() => toggleRole(role)}
|
|
172
|
+
>
|
|
173
|
+
<span className="users-role-title-content">
|
|
174
|
+
<Icon
|
|
175
|
+
className="users-role-chevron"
|
|
176
|
+
name={active ? "angle up" : "angle down"}
|
|
177
|
+
aria-hidden="true"
|
|
178
|
+
/>
|
|
179
|
+
<Header as="h3" className="users-role-title-header">
|
|
180
|
+
<Header.Content>
|
|
181
|
+
{formatMessage({ id: `user.type.${role}` })}
|
|
182
|
+
</Header.Content>
|
|
183
|
+
</Header>
|
|
184
|
+
<Label circular className="users-role-count">
|
|
185
|
+
{users.length}
|
|
186
|
+
</Label>
|
|
187
|
+
</span>
|
|
188
|
+
</button>
|
|
189
|
+
{index === 0 && results}
|
|
190
|
+
</Accordion.Title>
|
|
191
|
+
<Accordion.Content
|
|
192
|
+
id={panelId}
|
|
193
|
+
active={active}
|
|
194
|
+
className="users-role-content"
|
|
195
|
+
>
|
|
196
|
+
<Card.Group className="users-role-card-group">
|
|
197
|
+
{users.map((user, i) => (
|
|
198
|
+
<UserCard key={user.id || i} user={user} />
|
|
199
|
+
))}
|
|
200
|
+
</Card.Group>
|
|
201
|
+
</Accordion.Content>
|
|
202
|
+
</Fragment>
|
|
203
|
+
);
|
|
204
|
+
})}
|
|
205
|
+
</Accordion>
|
|
206
|
+
)}
|
|
207
|
+
{_.isEmpty(users) ? (
|
|
53
208
|
<Message
|
|
54
209
|
className="users-message"
|
|
55
210
|
header={
|
|
@@ -66,13 +221,11 @@ export const UserCards = ({ users: allUsers, showFullName }) => {
|
|
|
66
221
|
|
|
67
222
|
UserCards.propTypes = {
|
|
68
223
|
users: PropTypes.array,
|
|
69
|
-
showFullName: PropTypes.bool,
|
|
70
224
|
};
|
|
71
225
|
|
|
72
226
|
export const mapStateToProps = (state) => ({
|
|
73
227
|
users: state.users,
|
|
74
228
|
UserCard: state.userCard,
|
|
75
|
-
showFullName: state.userCardFullName,
|
|
76
229
|
});
|
|
77
230
|
|
|
78
231
|
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;
|