@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.
Files changed (78) hide show
  1. package/package.json +4 -4
  2. package/src/groups/components/GroupCard.js +2 -2
  3. package/src/groups/components/GroupCards.js +129 -37
  4. package/src/groups/components/GroupForm.js +13 -7
  5. package/src/groups/components/GroupFormModal.js +8 -7
  6. package/src/groups/components/GroupInfoModal.js +6 -6
  7. package/src/groups/components/GroupMemberPicker.js +2 -4
  8. package/src/groups/components/GroupRouteModal.js +15 -10
  9. package/src/groups/components/__tests__/GroupCards.spec.js +94 -0
  10. package/src/groups/components/__tests__/GroupForm.spec.js +2 -2
  11. package/src/groups/components/__tests__/GroupModals.spec.js +42 -16
  12. package/src/groups/components/__tests__/__snapshots__/GroupCards.spec.js.snap +14 -0
  13. package/src/messages/en.js +6 -5
  14. package/src/messages/es.js +8 -7
  15. package/src/styles/Groups.less +96 -131
  16. package/src/styles/UserCard.less +416 -0
  17. package/src/styles/UserModals.less +831 -0
  18. package/src/styles/Users.less +313 -0
  19. package/src/users/components/AdminUserRoutes.js +7 -20
  20. package/src/users/components/EditUser.js +7 -34
  21. package/src/users/components/EditUserModal.js +150 -0
  22. package/src/users/components/NewUser.js +128 -17
  23. package/src/users/components/Password.js +11 -189
  24. package/src/users/components/UserAcls.js +117 -6
  25. package/src/users/components/UserActions.js +99 -34
  26. package/src/users/components/UserCard.js +171 -22
  27. package/src/users/components/UserCards.js +188 -35
  28. package/src/users/components/UserFormFields.js +206 -0
  29. package/src/users/components/UserGroupAclRow.js +8 -2
  30. package/src/users/components/UserGroupAcls.js +2 -2
  31. package/src/users/components/UserGroupLink.js +21 -0
  32. package/src/users/components/UserInfoModal.js +228 -0
  33. package/src/users/components/UserPasswordField.js +66 -0
  34. package/src/users/components/UserPasswordModal.js +210 -0
  35. package/src/users/components/UserRoutes.js +4 -13
  36. package/src/users/components/UserTabs.js +5 -2
  37. package/src/users/components/UserTypeSelector.js +109 -0
  38. package/src/users/components/UsersAndGroups.js +39 -5
  39. package/src/users/components/UsersTable.js +234 -0
  40. package/src/users/components/__tests__/EditUser.spec.js +84 -10
  41. package/src/users/components/__tests__/NewUser.spec.js +34 -4
  42. package/src/users/components/__tests__/Password.spec.js +53 -146
  43. package/src/users/components/__tests__/UserAcls.spec.js +62 -2
  44. package/src/users/components/__tests__/UserCard.spec.js +111 -5
  45. package/src/users/components/__tests__/UserCards.spec.js +149 -0
  46. package/src/users/components/__tests__/UserGroupAclRow.spec.js +19 -1
  47. package/src/users/components/__tests__/UserModals.spec.js +554 -0
  48. package/src/users/components/__tests__/UserRoutes.spec.js +8 -3
  49. package/src/users/components/__tests__/UserTypeSelector.spec.js +32 -0
  50. package/src/users/components/__tests__/UsersAndGroups.spec.js +17 -0
  51. package/src/users/components/__tests__/__snapshots__/UserCard.spec.js.snap +66 -37
  52. package/src/users/components/__tests__/__snapshots__/UserCards.spec.js.snap +375 -75
  53. package/src/users/components/__tests__/__snapshots__/UserGroupAclRow.spec.js.snap +7 -3
  54. package/src/users/components/__tests__/__snapshots__/UserRoutes.spec.js.snap +4 -13
  55. package/src/users/hooks/useSortedGroups.js +13 -0
  56. package/src/users/hooks/useUserModalOperation.js +55 -0
  57. package/src/users/reducers/__tests__/userRedirect.spec.js +14 -3
  58. package/src/users/reducers/index.js +1 -0
  59. package/src/users/reducers/userModalOperation.js +29 -0
  60. package/src/users/reducers/userRedirect.js +5 -4
  61. package/src/users/sagas/__tests__/updateUser.spec.js +19 -1
  62. package/src/users/sagas/createUser.js +12 -5
  63. package/src/users/sagas/deleteUser.js +12 -5
  64. package/src/users/sagas/updateUser.js +19 -8
  65. package/src/users/selectors/__tests__/filterUsers.spec.js +11 -0
  66. package/src/users/selectors/__tests__/getUserAcls.spec.js +1 -0
  67. package/src/users/selectors/__tests__/getUserGroupAcls.spec.js +2 -0
  68. package/src/users/selectors/filterUsers.js +9 -1
  69. package/src/users/selectors/getUserGroupAcls.js +1 -0
  70. package/src/users/components/User.js +0 -68
  71. package/src/users/components/UserPassword.js +0 -24
  72. package/src/users/components/__tests__/User.spec.js +0 -20
  73. package/src/users/components/__tests__/UserPassword.spec.js +0 -24
  74. package/src/users/components/__tests__/__snapshots__/EditUser.spec.js.snap +0 -281
  75. package/src/users/components/__tests__/__snapshots__/NewUser.spec.js.snap +0 -321
  76. package/src/users/components/__tests__/__snapshots__/Password.spec.js.snap +0 -128
  77. package/src/users/components/__tests__/__snapshots__/User.spec.js.snap +0 -129
  78. 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 { connect } from "react-redux";
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
- export const UserCard = ({
9
- user: { user_name, id, full_name, email, external_id },
10
- showFullName = true,
11
- }) => (
12
- <Card key={id}>
13
- <Card.Content>
14
- <Card.Header as={Link} to={linkTo.USER({ id })}>
15
- <Icon name="user" />
16
- {user_name}
17
- </Card.Header>
18
- <Card.Description>
19
- {showFullName ? full_name : external_id}
20
- <br />
21
- {email}
22
- </Card.Description>
23
- </Card.Content>
24
- </Card>
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 connect(null, { deleteUser })(UserCard);
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 { Button, Card, Header, Icon, Message } from "semantic-ui-react";
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
- export const UserCards = ({ users: allUsers, showFullName }) => {
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 users = _.flow(_.sortBy("user_name"), filterUsers(filter))(allUsers);
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
- <SearchInput
26
- onChange={(_e, data) => _.flow(_.propOr("", "value"), setFilter)(data)}
27
- placeholder={formatMessage({ id: "user.search.placeholder" })}
28
- value={filter}
29
- />
30
- <Button
31
- floated="right"
32
- primary
33
- content={formatMessage({ id: "users.actions.create" })}
34
- icon="add user"
35
- as={Link}
36
- to={USERS_NEW}
37
- />
38
- {usersByRole.map(([role, users]) => (
39
- <Fragment key={role}>
40
- <Header
41
- as="h3"
42
- dividing
43
- content={formatMessage({ id: `user.type.${role}` })}
44
- />
45
- <Card.Group>
46
- {users.map((user, i) => (
47
- <UserCard key={i} user={user} showFullname={showFullName} />
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
- </Card.Group>
50
- </Fragment>
51
- ))}
52
- {_.trim(filter) !== "" && _.isEmpty(users) ? (
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;