@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
|
@@ -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;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import PropTypes from "prop-types";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { connect } from "react-redux";
|
|
4
|
+
import { Checkbox, Icon } from "semantic-ui-react";
|
|
5
|
+
import { FormattedMessage, useIntl } from "react-intl";
|
|
6
|
+
import { Link } from "react-router";
|
|
7
|
+
import TrueModal from "@truedat/core/components/TrueModal";
|
|
8
|
+
import { linkTo } from "@truedat/core/routes";
|
|
9
|
+
import { useGroups } from "../../groups/hooks/useGroups";
|
|
10
|
+
import { getUserAcls, getUserGroupAcls } from "../selectors";
|
|
11
|
+
import { UnifiedAcls } from "./UserAcls";
|
|
12
|
+
import UserGroupLink from "./UserGroupLink";
|
|
13
|
+
import { userTypeOptions } from "./UserTypeSelector";
|
|
14
|
+
|
|
15
|
+
const getGroupName = (group) => {
|
|
16
|
+
if (typeof group === "string") return group;
|
|
17
|
+
return group?.alias || group?.name || String(group);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const UserInfoModal = ({
|
|
21
|
+
user,
|
|
22
|
+
groups = [],
|
|
23
|
+
acls,
|
|
24
|
+
groupAcls,
|
|
25
|
+
onClose,
|
|
26
|
+
}) => {
|
|
27
|
+
const { formatMessage } = useIntl();
|
|
28
|
+
const [groupByRole, setGroupByRole] = useState(false);
|
|
29
|
+
const [aclsOpen, setAclsOpen] = useState(true);
|
|
30
|
+
|
|
31
|
+
if (!user?.id) return null;
|
|
32
|
+
|
|
33
|
+
const displayName = user.full_name?.trim() || user.user_name || "";
|
|
34
|
+
const nameParts = displayName.split(/\s+/);
|
|
35
|
+
const initials =
|
|
36
|
+
(nameParts[0]?.[0] || "?") +
|
|
37
|
+
(nameParts.length > 1 ? nameParts[nameParts.length - 1][0] : "");
|
|
38
|
+
const userType = userTypeOptions.find(({ value }) => value === user.role);
|
|
39
|
+
const unavailable = formatMessage({ id: "user.info.unavailable" });
|
|
40
|
+
const permissions = [...(acls || []), ...(groupAcls || [])];
|
|
41
|
+
const roleCount = new Set(permissions.map(({ role }) => role)).size;
|
|
42
|
+
const hasAcls = permissions.length > 0;
|
|
43
|
+
const aclsPanelId = `user-acls-panel-${user.id}`;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<TrueModal
|
|
47
|
+
open
|
|
48
|
+
title={<FormattedMessage id="user.info.title" />}
|
|
49
|
+
icon="info circle"
|
|
50
|
+
actions={[
|
|
51
|
+
{
|
|
52
|
+
key: "close",
|
|
53
|
+
content: formatMessage({
|
|
54
|
+
id: "actions.close",
|
|
55
|
+
defaultMessage: "Close",
|
|
56
|
+
}),
|
|
57
|
+
secondary: true,
|
|
58
|
+
onClick: onClose,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
key: "edit",
|
|
62
|
+
content: formatMessage({ id: "actions.edit" }),
|
|
63
|
+
primary: true,
|
|
64
|
+
as: Link,
|
|
65
|
+
to: linkTo.USER_EDIT({ id: user.id }),
|
|
66
|
+
},
|
|
67
|
+
]}
|
|
68
|
+
onClose={onClose}
|
|
69
|
+
size="small"
|
|
70
|
+
className="user-modal user-modal--info"
|
|
71
|
+
>
|
|
72
|
+
<div className="um-profile">
|
|
73
|
+
<div className="um-avatar" aria-hidden="true">
|
|
74
|
+
{initials.toUpperCase()}
|
|
75
|
+
</div>
|
|
76
|
+
<div className="um-profile-identity">
|
|
77
|
+
<h2 className="um-profile-name">{displayName}</h2>
|
|
78
|
+
<p>
|
|
79
|
+
<Icon name="mail" size="small" aria-hidden="true" />
|
|
80
|
+
{user.email || <em className="um-unavailable">{unavailable}</em>}
|
|
81
|
+
</p>
|
|
82
|
+
</div>
|
|
83
|
+
{userType ? (
|
|
84
|
+
<span className="um-user-type-box um-user-type-box--active">
|
|
85
|
+
<Icon name={userType.icon} aria-hidden="true" />
|
|
86
|
+
{formatMessage({ id: userType.messageId })}
|
|
87
|
+
</span>
|
|
88
|
+
) : (
|
|
89
|
+
<em className="um-unavailable">{user.role || unavailable}</em>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
<dl className="um-profile-fields">
|
|
93
|
+
{[
|
|
94
|
+
["user circle", "user.form.user_name", user.user_name],
|
|
95
|
+
["id badge", "user.form.external_id", user.external_id],
|
|
96
|
+
].map(([icon, messageId, value]) => (
|
|
97
|
+
<div className="um-profile-field" key={messageId}>
|
|
98
|
+
<dt>
|
|
99
|
+
<Icon name={icon} aria-hidden="true" />
|
|
100
|
+
{formatMessage({ id: messageId })}
|
|
101
|
+
</dt>
|
|
102
|
+
<dd>
|
|
103
|
+
{value || <em className="um-unavailable">{unavailable}</em>}
|
|
104
|
+
</dd>
|
|
105
|
+
</div>
|
|
106
|
+
))}
|
|
107
|
+
</dl>
|
|
108
|
+
{user.groups?.length ? (
|
|
109
|
+
<section className="um-profile-groups">
|
|
110
|
+
<h3 className="um-section-label">
|
|
111
|
+
<span>
|
|
112
|
+
<Icon name="users" aria-hidden="true" />
|
|
113
|
+
</span>
|
|
114
|
+
<span>
|
|
115
|
+
<FormattedMessage id="user.form.groups" />
|
|
116
|
+
<span className="um-acl-count">({user.groups.length})</span>
|
|
117
|
+
</span>
|
|
118
|
+
</h3>
|
|
119
|
+
<ul className="um-groups">
|
|
120
|
+
{user.groups.map((group, index) => {
|
|
121
|
+
const name = typeof group === "string" ? group : group?.name;
|
|
122
|
+
const alias =
|
|
123
|
+
typeof group === "string" ? group : group?.alias || name;
|
|
124
|
+
const namedGroup = groups.find(
|
|
125
|
+
(candidate) => name != null && candidate.name === name,
|
|
126
|
+
);
|
|
127
|
+
const aliasGroups = groups.filter(
|
|
128
|
+
(candidate) => alias && candidate.alias === alias,
|
|
129
|
+
);
|
|
130
|
+
const id =
|
|
131
|
+
group?.id ??
|
|
132
|
+
namedGroup?.id ??
|
|
133
|
+
(aliasGroups.length === 1 ? aliasGroups[0].id : undefined);
|
|
134
|
+
return (
|
|
135
|
+
<li key={id ?? index} className="um-group-chip">
|
|
136
|
+
<UserGroupLink id={id} name={getGroupName(group)} />
|
|
137
|
+
</li>
|
|
138
|
+
);
|
|
139
|
+
})}
|
|
140
|
+
</ul>
|
|
141
|
+
</section>
|
|
142
|
+
) : (
|
|
143
|
+
<p className="um-empty">
|
|
144
|
+
<Icon name="users" aria-hidden="true" />
|
|
145
|
+
<FormattedMessage id="user.info.no_groups" />
|
|
146
|
+
</p>
|
|
147
|
+
)}
|
|
148
|
+
|
|
149
|
+
<div className="um-permissions">
|
|
150
|
+
{hasAcls ? (
|
|
151
|
+
<div className="um-acl-section">
|
|
152
|
+
<div className="um-acl-toolbar">
|
|
153
|
+
<button
|
|
154
|
+
type="button"
|
|
155
|
+
className="um-acl-header"
|
|
156
|
+
aria-expanded={aclsOpen}
|
|
157
|
+
aria-controls={aclsPanelId}
|
|
158
|
+
onClick={() => setAclsOpen((open) => !open)}
|
|
159
|
+
>
|
|
160
|
+
<span className="um-acl-icon--primary">
|
|
161
|
+
<Icon name="shield alternate" />
|
|
162
|
+
</span>
|
|
163
|
+
<span className="um-acl-title">
|
|
164
|
+
<FormattedMessage id="user.acl" />
|
|
165
|
+
<span className="um-acl-count">({roleCount})</span>
|
|
166
|
+
</span>
|
|
167
|
+
<Icon name={aclsOpen ? "chevron up" : "chevron down"} />
|
|
168
|
+
</button>
|
|
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
|
+
)}
|
|
180
|
+
</div>
|
|
181
|
+
{aclsOpen && (
|
|
182
|
+
<div
|
|
183
|
+
id={aclsPanelId}
|
|
184
|
+
className="um-acl-panel"
|
|
185
|
+
role="region"
|
|
186
|
+
aria-label={formatMessage({
|
|
187
|
+
id: "user.acl",
|
|
188
|
+
})}
|
|
189
|
+
tabIndex={0}
|
|
190
|
+
>
|
|
191
|
+
<UnifiedAcls
|
|
192
|
+
acls={permissions}
|
|
193
|
+
groupBy={groupByRole && roleCount > 1 ? "role" : "group"}
|
|
194
|
+
/>
|
|
195
|
+
</div>
|
|
196
|
+
)}
|
|
197
|
+
</div>
|
|
198
|
+
) : (
|
|
199
|
+
<p className="um-empty">
|
|
200
|
+
<Icon name="shield alternate" aria-hidden="true" />
|
|
201
|
+
<FormattedMessage id="user.info.no_permissions" />
|
|
202
|
+
</p>
|
|
203
|
+
)}
|
|
204
|
+
</div>
|
|
205
|
+
</TrueModal>
|
|
206
|
+
);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
UserInfoModal.propTypes = {
|
|
210
|
+
user: PropTypes.object,
|
|
211
|
+
groups: PropTypes.array,
|
|
212
|
+
acls: PropTypes.array,
|
|
213
|
+
groupAcls: PropTypes.array,
|
|
214
|
+
onClose: PropTypes.func.isRequired,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const UserInfoModalWithGroups = (props) => {
|
|
218
|
+
const { groups } = useGroups();
|
|
219
|
+
return <UserInfoModal {...props} groups={groups} />;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const mapStateToProps = (state) => ({
|
|
223
|
+
user: state.user,
|
|
224
|
+
acls: getUserAcls(state),
|
|
225
|
+
groupAcls: getUserGroupAcls(state),
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
export default connect(mapStateToProps)(UserInfoModalWithGroups);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import PropTypes from "prop-types";
|
|
3
|
+
import { Controller } from "react-hook-form";
|
|
4
|
+
import { Form, Icon } from "semantic-ui-react";
|
|
5
|
+
|
|
6
|
+
const Feedback = ({ id, message }) => {
|
|
7
|
+
const [lastMessage, setLastMessage] = useState(message);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
if (message) setLastMessage(message);
|
|
10
|
+
}, [message]);
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div className="um-field-feedback" id={id} aria-live="polite">
|
|
14
|
+
<span className={message ? "is-visible" : ""} aria-hidden={!message}>
|
|
15
|
+
{lastMessage}
|
|
16
|
+
</span>
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
Feedback.propTypes = { id: PropTypes.string, message: PropTypes.string };
|
|
22
|
+
|
|
23
|
+
const UserPasswordField = ({
|
|
24
|
+
control,
|
|
25
|
+
name,
|
|
26
|
+
label,
|
|
27
|
+
rules,
|
|
28
|
+
disabled,
|
|
29
|
+
autoComplete = "new-password",
|
|
30
|
+
}) => (
|
|
31
|
+
<Controller
|
|
32
|
+
control={control}
|
|
33
|
+
name={name}
|
|
34
|
+
rules={rules}
|
|
35
|
+
render={({ field, fieldState: { error } }) => (
|
|
36
|
+
<Form.Field required disabled={disabled} error={Boolean(error)}>
|
|
37
|
+
<label htmlFor={name}>{label}</label>
|
|
38
|
+
<div className="ui left icon input">
|
|
39
|
+
<input
|
|
40
|
+
{...field}
|
|
41
|
+
id={name}
|
|
42
|
+
type="password"
|
|
43
|
+
autoComplete={autoComplete}
|
|
44
|
+
disabled={disabled}
|
|
45
|
+
required
|
|
46
|
+
aria-invalid={Boolean(error)}
|
|
47
|
+
aria-describedby={`${name}-feedback`}
|
|
48
|
+
/>
|
|
49
|
+
<Icon name="lock" aria-hidden="true" />
|
|
50
|
+
</div>
|
|
51
|
+
<Feedback id={`${name}-feedback`} message={error?.message} />
|
|
52
|
+
</Form.Field>
|
|
53
|
+
)}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
UserPasswordField.propTypes = {
|
|
58
|
+
control: PropTypes.object.isRequired,
|
|
59
|
+
name: PropTypes.string.isRequired,
|
|
60
|
+
label: PropTypes.string.isRequired,
|
|
61
|
+
rules: PropTypes.object,
|
|
62
|
+
disabled: PropTypes.bool,
|
|
63
|
+
autoComplete: PropTypes.string,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export default UserPasswordField;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { flushSync } from "react-dom";
|
|
3
|
+
import PropTypes from "prop-types";
|
|
4
|
+
import { connect } from "react-redux";
|
|
5
|
+
import { Form } from "semantic-ui-react";
|
|
6
|
+
import { FormattedMessage, useIntl } from "react-intl";
|
|
7
|
+
import { useForm, useWatch } from "react-hook-form";
|
|
8
|
+
import TrueModal from "@truedat/core/components/TrueModal";
|
|
9
|
+
import { updatePassword } from "../routines";
|
|
10
|
+
import useUserModalOperation from "../hooks/useUserModalOperation";
|
|
11
|
+
|
|
12
|
+
import UserPasswordField from "./UserPasswordField";
|
|
13
|
+
|
|
14
|
+
const MIN_LENGTH = 6;
|
|
15
|
+
const EMPTY_PASSWORDS = {
|
|
16
|
+
currentPassword: "",
|
|
17
|
+
password: "",
|
|
18
|
+
passwordConfirmation: "",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const getInitials = (fullName, userName) => {
|
|
22
|
+
const normalized = fullName?.trim() || "";
|
|
23
|
+
if (normalized) {
|
|
24
|
+
const parts = normalized.split(/\s+/);
|
|
25
|
+
return parts.length >= 2
|
|
26
|
+
? (parts[0][0] + parts[parts.length - 1][0]).toUpperCase()
|
|
27
|
+
: parts[0][0].toUpperCase();
|
|
28
|
+
}
|
|
29
|
+
return userName ? userName[0].toUpperCase() : "?";
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const UserPasswordModal = ({ user, authUserName, onClose }) => {
|
|
33
|
+
const { formatMessage } = useIntl();
|
|
34
|
+
const discardTimer = useRef(null);
|
|
35
|
+
useEffect(() => () => clearTimeout(discardTimer.current), []);
|
|
36
|
+
const {
|
|
37
|
+
handleSubmit,
|
|
38
|
+
control,
|
|
39
|
+
reset,
|
|
40
|
+
getValues,
|
|
41
|
+
trigger,
|
|
42
|
+
formState: { isValid },
|
|
43
|
+
} = useForm({ mode: "all", defaultValues: EMPTY_PASSWORDS });
|
|
44
|
+
const password = useWatch({ control, name: "password" });
|
|
45
|
+
const discardAndClose = () => {
|
|
46
|
+
if (discardTimer.current !== null) return;
|
|
47
|
+
// Chrome retains typed passwords after value assignments. Native editing
|
|
48
|
+
// clears that state too; allow it to settle before removing the form/routing.
|
|
49
|
+
if (document.execCommand) {
|
|
50
|
+
document
|
|
51
|
+
.getElementById("password-modal-form")
|
|
52
|
+
?.querySelectorAll('input[type="password"]')
|
|
53
|
+
.forEach((input) => {
|
|
54
|
+
if (!input.value) return;
|
|
55
|
+
input.focus({ preventScroll: true });
|
|
56
|
+
input.select();
|
|
57
|
+
document.execCommand("delete");
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
flushSync(() => reset(EMPTY_PASSWORDS));
|
|
61
|
+
discardTimer.current = setTimeout(() => {
|
|
62
|
+
discardTimer.current = null;
|
|
63
|
+
onClose();
|
|
64
|
+
}, 0);
|
|
65
|
+
};
|
|
66
|
+
const {
|
|
67
|
+
submit,
|
|
68
|
+
isSubmitting,
|
|
69
|
+
onClose: closeModal,
|
|
70
|
+
error,
|
|
71
|
+
} = useUserModalOperation(updatePassword, discardAndClose, onClose);
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (getValues("passwordConfirmation")) void trigger("passwordConfirmation");
|
|
75
|
+
}, [password, getValues, trigger]);
|
|
76
|
+
|
|
77
|
+
const needsCurrentPassword =
|
|
78
|
+
!!authUserName && user?.user_name === authUserName;
|
|
79
|
+
|
|
80
|
+
if (!user?.id && !(authUserName && needsCurrentPassword)) return null;
|
|
81
|
+
const passwordRules = {
|
|
82
|
+
required: formatMessage({ id: "user.password.required" }),
|
|
83
|
+
minLength: {
|
|
84
|
+
value: MIN_LENGTH,
|
|
85
|
+
message: formatMessage(
|
|
86
|
+
{ id: "user.password.min_length" },
|
|
87
|
+
{ count: MIN_LENGTH },
|
|
88
|
+
),
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const onSubmit = ({ currentPassword, password }) => {
|
|
93
|
+
if (discardTimer.current !== null) return;
|
|
94
|
+
const userData = needsCurrentPassword
|
|
95
|
+
? { new_password: password, old_password: currentPassword }
|
|
96
|
+
: { id: user.id, new_password: password };
|
|
97
|
+
submit({ user: userData });
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const initials = getInitials(user.full_name, user.user_name);
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<TrueModal
|
|
104
|
+
open
|
|
105
|
+
title={formatMessage({ id: "user.password.change" })}
|
|
106
|
+
icon="lock"
|
|
107
|
+
actions={[
|
|
108
|
+
{
|
|
109
|
+
key: "cancel",
|
|
110
|
+
content: formatMessage({ id: "actions.cancel" }),
|
|
111
|
+
secondary: true,
|
|
112
|
+
onClick: closeModal,
|
|
113
|
+
disabled: isSubmitting,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
key: "save",
|
|
117
|
+
content: formatMessage({ id: "user.password.change" }),
|
|
118
|
+
primary: true,
|
|
119
|
+
type: "submit",
|
|
120
|
+
form: "password-modal-form",
|
|
121
|
+
disabled: !isValid,
|
|
122
|
+
loading: isSubmitting,
|
|
123
|
+
},
|
|
124
|
+
]}
|
|
125
|
+
onClose={closeModal}
|
|
126
|
+
aria-busy={isSubmitting}
|
|
127
|
+
closeOnEscape={!isSubmitting}
|
|
128
|
+
closeOnDimmerClick={!isSubmitting}
|
|
129
|
+
banner={error ? <span role="alert">{error}</span> : null}
|
|
130
|
+
bannerType="danger"
|
|
131
|
+
size="small"
|
|
132
|
+
className="user-modal user-modal--password"
|
|
133
|
+
>
|
|
134
|
+
<div className="um-password-user">
|
|
135
|
+
<div className="um-avatar um-avatar--sm">{initials}</div>
|
|
136
|
+
<div>
|
|
137
|
+
<h2 className="um-profile-name">
|
|
138
|
+
{user.full_name?.trim() || user.user_name}
|
|
139
|
+
</h2>
|
|
140
|
+
<div className="um-password-email">
|
|
141
|
+
{user.email || (
|
|
142
|
+
<em className="um-unavailable">
|
|
143
|
+
<FormattedMessage id="user.info.unavailable" />
|
|
144
|
+
</em>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<Form
|
|
151
|
+
id="password-modal-form"
|
|
152
|
+
noValidate
|
|
153
|
+
onSubmit={handleSubmit(onSubmit)}
|
|
154
|
+
>
|
|
155
|
+
<input
|
|
156
|
+
className="um-accessible-field"
|
|
157
|
+
type="text"
|
|
158
|
+
name="username"
|
|
159
|
+
autoComplete="username"
|
|
160
|
+
value={user.user_name || authUserName || ""}
|
|
161
|
+
readOnly
|
|
162
|
+
tabIndex={-1}
|
|
163
|
+
aria-hidden="true"
|
|
164
|
+
/>
|
|
165
|
+
{needsCurrentPassword && (
|
|
166
|
+
<UserPasswordField
|
|
167
|
+
control={control}
|
|
168
|
+
name="currentPassword"
|
|
169
|
+
label={formatMessage({ id: "user.password.current" })}
|
|
170
|
+
rules={passwordRules}
|
|
171
|
+
disabled={isSubmitting}
|
|
172
|
+
autoComplete="current-password"
|
|
173
|
+
/>
|
|
174
|
+
)}
|
|
175
|
+
<UserPasswordField
|
|
176
|
+
control={control}
|
|
177
|
+
name="password"
|
|
178
|
+
label={formatMessage({ id: "user.password.new_password" })}
|
|
179
|
+
rules={passwordRules}
|
|
180
|
+
disabled={isSubmitting}
|
|
181
|
+
/>
|
|
182
|
+
<UserPasswordField
|
|
183
|
+
control={control}
|
|
184
|
+
name="passwordConfirmation"
|
|
185
|
+
label={formatMessage({ id: "user.form.password_confirmation" })}
|
|
186
|
+
rules={{
|
|
187
|
+
required: formatMessage({ id: "user.password.required" }),
|
|
188
|
+
validate: (value) =>
|
|
189
|
+
value === getValues("password") ||
|
|
190
|
+
formatMessage({ id: "user.password.mismatch" }),
|
|
191
|
+
}}
|
|
192
|
+
disabled={isSubmitting}
|
|
193
|
+
/>
|
|
194
|
+
</Form>
|
|
195
|
+
</TrueModal>
|
|
196
|
+
);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
UserPasswordModal.propTypes = {
|
|
200
|
+
user: PropTypes.object,
|
|
201
|
+
authUserName: PropTypes.string,
|
|
202
|
+
onClose: PropTypes.func.isRequired,
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const mapStateToProps = ({ user, authentication }) => ({
|
|
206
|
+
user,
|
|
207
|
+
authUserName: authentication?.user_name,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
export default connect(mapStateToProps)(UserPasswordModal);
|
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
import { Routes, Route, Outlet } from "react-router";
|
|
2
2
|
import { ProtectedRoute, Loader } from "@truedat/core/router";
|
|
3
3
|
import GroupRouteModal from "../../groups/components/GroupRouteModal";
|
|
4
|
-
|
|
5
4
|
import Password from "./Password";
|
|
6
|
-
import EditUser from "./EditUser";
|
|
7
|
-
import NewUser from "./NewUser";
|
|
8
|
-
import User from "./User";
|
|
9
|
-
import UserLoader from "./UserLoader";
|
|
10
5
|
import UsersAndGroups from "./UsersAndGroups";
|
|
11
6
|
import UsersLoader from "./UsersLoader";
|
|
12
|
-
import UserPassword from "./UserPassword";
|
|
13
7
|
|
|
14
8
|
export const UserRoutes = () => (
|
|
15
9
|
<Routes>
|
|
@@ -22,13 +16,10 @@ export const UserRoutes = () => (
|
|
|
22
16
|
}
|
|
23
17
|
>
|
|
24
18
|
<Route index element={<UsersAndGroups />} />
|
|
25
|
-
<Route path={"new"} element={<
|
|
26
|
-
|
|
27
|
-
<Route path={":id"} element={<
|
|
28
|
-
|
|
29
|
-
<Route path={"edit"} element={<EditUser />} />
|
|
30
|
-
<Route path={"password"} element={<UserPassword />} />
|
|
31
|
-
</Route>
|
|
19
|
+
<Route path={"new"} element={<UsersAndGroups />} />
|
|
20
|
+
<Route path={":id"} element={<UsersAndGroups />} />
|
|
21
|
+
<Route path={":id/edit"} element={<UsersAndGroups />} />
|
|
22
|
+
<Route path={":id/password"} element={<UsersAndGroups />} />
|
|
32
23
|
</Route>
|
|
33
24
|
<Route
|
|
34
25
|
path={"groups"}
|
|
@@ -6,12 +6,15 @@ import { GROUPS, USERS } from "@truedat/core/routes";
|
|
|
6
6
|
|
|
7
7
|
const UserTabs = () => {
|
|
8
8
|
const path = usePath();
|
|
9
|
+
const isUsersPath = path === USERS || path.startsWith(USERS + "/");
|
|
10
|
+
const isGroupsPath = path === GROUPS || path.startsWith(GROUPS + "/");
|
|
11
|
+
|
|
9
12
|
return (
|
|
10
13
|
<Menu attached="top" secondary pointing tabular>
|
|
11
|
-
<Menu.Item active={
|
|
14
|
+
<Menu.Item active={isUsersPath} as={Link} to={USERS}>
|
|
12
15
|
<FormattedMessage id="tabs.users" />
|
|
13
16
|
</Menu.Item>
|
|
14
|
-
<Menu.Item active={
|
|
17
|
+
<Menu.Item active={isGroupsPath} as={Link} to={GROUPS}>
|
|
15
18
|
<FormattedMessage id="tabs.groups" />
|
|
16
19
|
</Menu.Item>
|
|
17
20
|
</Menu>
|