@truedat/auth 8.14.2 → 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.
Files changed (66) hide show
  1. package/package.json +4 -4
  2. package/src/messages/en.js +6 -5
  3. package/src/messages/es.js +8 -7
  4. package/src/styles/UserCard.less +582 -0
  5. package/src/styles/UserModals.less +826 -0
  6. package/src/styles/Users.less +280 -0
  7. package/src/users/components/AdminUserRoutes.js +7 -20
  8. package/src/users/components/EditUser.js +7 -34
  9. package/src/users/components/EditUserModal.js +150 -0
  10. package/src/users/components/NewUser.js +128 -17
  11. package/src/users/components/Password.js +11 -189
  12. package/src/users/components/UserAcls.js +56 -6
  13. package/src/users/components/UserActions.js +99 -34
  14. package/src/users/components/UserCard.js +168 -22
  15. package/src/users/components/UserCards.js +273 -35
  16. package/src/users/components/UserFormFields.js +206 -0
  17. package/src/users/components/UserGroupAclRow.js +8 -2
  18. package/src/users/components/UserGroupAcls.js +2 -2
  19. package/src/users/components/UserGroupLink.js +21 -0
  20. package/src/users/components/UserInfoModal.js +268 -0
  21. package/src/users/components/UserPasswordField.js +66 -0
  22. package/src/users/components/UserPasswordModal.js +210 -0
  23. package/src/users/components/UserRoutes.js +4 -13
  24. package/src/users/components/UserTabs.js +5 -2
  25. package/src/users/components/UserTypeSelector.js +109 -0
  26. package/src/users/components/UsersAndGroups.js +39 -5
  27. package/src/users/components/UsersTable.js +234 -0
  28. package/src/users/components/__tests__/EditUser.spec.js +84 -10
  29. package/src/users/components/__tests__/NewUser.spec.js +34 -4
  30. package/src/users/components/__tests__/Password.spec.js +53 -146
  31. package/src/users/components/__tests__/UserAcls.spec.js +23 -2
  32. package/src/users/components/__tests__/UserCard.spec.js +111 -5
  33. package/src/users/components/__tests__/UserCards.spec.js +194 -0
  34. package/src/users/components/__tests__/UserGroupAclRow.spec.js +19 -1
  35. package/src/users/components/__tests__/UserModals.spec.js +507 -0
  36. package/src/users/components/__tests__/UserRoutes.spec.js +8 -3
  37. package/src/users/components/__tests__/UserTypeSelector.spec.js +32 -0
  38. package/src/users/components/__tests__/UsersAndGroups.spec.js +17 -0
  39. package/src/users/components/__tests__/__snapshots__/UserCard.spec.js.snap +64 -37
  40. package/src/users/components/__tests__/__snapshots__/UserCards.spec.js.snap +424 -75
  41. package/src/users/components/__tests__/__snapshots__/UserGroupAclRow.spec.js.snap +7 -3
  42. package/src/users/components/__tests__/__snapshots__/UserRoutes.spec.js.snap +4 -13
  43. package/src/users/hooks/useSortedGroups.js +13 -0
  44. package/src/users/hooks/useUserModalOperation.js +55 -0
  45. package/src/users/reducers/__tests__/userRedirect.spec.js +14 -3
  46. package/src/users/reducers/index.js +1 -0
  47. package/src/users/reducers/userModalOperation.js +29 -0
  48. package/src/users/reducers/userRedirect.js +5 -4
  49. package/src/users/sagas/__tests__/updateUser.spec.js +19 -1
  50. package/src/users/sagas/createUser.js +12 -5
  51. package/src/users/sagas/deleteUser.js +12 -5
  52. package/src/users/sagas/updateUser.js +19 -8
  53. package/src/users/selectors/__tests__/filterUsers.spec.js +11 -0
  54. package/src/users/selectors/__tests__/getUserAcls.spec.js +1 -0
  55. package/src/users/selectors/__tests__/getUserGroupAcls.spec.js +2 -0
  56. package/src/users/selectors/filterUsers.js +9 -1
  57. package/src/users/selectors/getUserGroupAcls.js +1 -0
  58. package/src/users/components/User.js +0 -68
  59. package/src/users/components/UserPassword.js +0 -24
  60. package/src/users/components/__tests__/User.spec.js +0 -20
  61. package/src/users/components/__tests__/UserPassword.spec.js +0 -24
  62. package/src/users/components/__tests__/__snapshots__/EditUser.spec.js.snap +0 -281
  63. package/src/users/components/__tests__/__snapshots__/NewUser.spec.js.snap +0 -321
  64. package/src/users/components/__tests__/__snapshots__/Password.spec.js.snap +0 -128
  65. package/src/users/components/__tests__/__snapshots__/User.spec.js.snap +0 -129
  66. package/src/users/components/__tests__/__snapshots__/UserPassword.spec.js.snap +0 -124
@@ -1,193 +1,16 @@
1
- import _ from "lodash/fp";
2
1
  import PropTypes from "prop-types";
3
- import { Container, Header, Icon, Segment } from "semantic-ui-react";
4
- import { FormattedMessage, useIntl } from "react-intl";
5
2
  import { connect } from "react-redux";
6
- import { useForm, Controller } from "react-hook-form";
7
- import { Button, Form } from "semantic-ui-react";
8
- import HistoryBackButton from "@truedat/core/components/HistoryBackButton";
9
- import { updatePassword } from "../routines";
10
-
11
- const MIN_LENGTH = 6;
12
-
13
- export const Password = ({
14
- id,
15
- userName,
16
- authUserName,
17
- updatePassword,
18
- passwordUpdating,
19
- }) => {
20
- const { formatMessage } = useIntl();
21
- const {
22
- handleSubmit,
23
- control,
24
- formState: { errors, isValid },
25
- watch,
26
- } = useForm({
27
- mode: "all",
28
- defaultValues: {
29
- currentPassword: "",
30
- password: "",
31
- passwordConfirmation: "",
32
- },
33
- });
34
-
35
- const needsCurrentPassword = _.isEmpty(userName) || userName === authUserName;
36
-
37
- const onSubmit = ({ currentPassword, password }) => {
38
- const user = needsCurrentPassword
39
- ? { new_password: password, old_password: currentPassword }
40
- : { id: id, new_password: password };
41
- updatePassword({ user });
42
- };
43
-
44
- const passwordConfirmationValidator = (value) =>
45
- value === watch("password") ||
46
- formatMessage({ id: "user.password.must.match" });
3
+ import { useNavigate } from "react-router";
4
+ import { UserPasswordModal } from "./UserPasswordModal";
47
5
 
6
+ export const Password = ({ id, userName, authUserName }) => {
7
+ const navigate = useNavigate();
48
8
  return (
49
- <Container text as={Segment}>
50
- <Header as="h2">
51
- <Icon
52
- circular
53
- name={formatMessage({
54
- id: "user.password.header.icon",
55
- defaultMessage: "user",
56
- })}
57
- />
58
- <Header.Content>
59
- {_.isEmpty(userName) ? (
60
- <FormattedMessage id="user.password.header" />
61
- ) : (
62
- userName
63
- )}
64
- <Header.Subheader>
65
- <FormattedMessage id="user.password.change" />
66
- </Header.Subheader>
67
- </Header.Content>
68
- </Header>
69
- <Segment attached="bottom">
70
- <Form onSubmit={handleSubmit(onSubmit)}>
71
- {needsCurrentPassword ? (
72
- <Controller
73
- control={control}
74
- name="currentPassword"
75
- rules={{
76
- minLength: {
77
- value: MIN_LENGTH,
78
- message: formatMessage(
79
- { id: "form.validation.minLength" },
80
- {
81
- prop: formatMessage({ id: "user.password.current" }),
82
- value: MIN_LENGTH,
83
- },
84
- ),
85
- },
86
- required: formatMessage(
87
- { id: "form.validation.required" },
88
- { prop: formatMessage({ id: "user.password.current" }) },
89
- ),
90
- }}
91
- render={({ field: { onChange, onBlur, value } }) => (
92
- <Form.Input
93
- autoComplete="off"
94
- error={
95
- errors.currentPassword?.type === "minLength" &&
96
- errors.currentPassword.message
97
- }
98
- icon="lock"
99
- iconPosition="left"
100
- id="currentPassword"
101
- label={formatMessage({ id: "user.password.current" })}
102
- onChange={onChange}
103
- onBlur={onBlur}
104
- value={value}
105
- type="password"
106
- required
107
- />
108
- )}
109
- />
110
- ) : null}
111
- <Controller
112
- control={control}
113
- name="password"
114
- rules={{
115
- minLength: {
116
- value: MIN_LENGTH,
117
- message: formatMessage(
118
- { id: "form.validation.minLength" },
119
- {
120
- prop: formatMessage({ id: "user.password.new_password" }),
121
- value: MIN_LENGTH,
122
- },
123
- ),
124
- },
125
- required: formatMessage(
126
- { id: "form.validation.required" },
127
- { prop: formatMessage({ id: "user.password.new_password" }) },
128
- ),
129
- }}
130
- render={({ field: { onBlur, onChange, value } }) => (
131
- <Form.Input
132
- autoComplete="off"
133
- error={
134
- errors.password?.type === "minLength" &&
135
- errors.password.message
136
- }
137
- icon="lock"
138
- iconPosition="left"
139
- id="password"
140
- label={formatMessage({ id: "user.password.new_password" })}
141
- onChange={onChange}
142
- onBlur={onBlur}
143
- value={value}
144
- type="password"
145
- required
146
- />
147
- )}
148
- />
149
- <Controller
150
- control={control}
151
- name="passwordConfirmation"
152
- rules={{
153
- validate: passwordConfirmationValidator,
154
- required: true,
155
- }}
156
- render={({ field: { onBlur, onChange, value } }) => (
157
- <Form.Input
158
- autoComplete="off"
159
- error={
160
- errors.passwordConfirmation?.type === "validate" &&
161
- errors.passwordConfirmation.message
162
- }
163
- icon="lock"
164
- iconPosition="left"
165
- id="passwordConfirmation"
166
- label={formatMessage({ id: "login.form.re_password.label" })}
167
- onChange={onChange}
168
- onBlur={onBlur}
169
- value={value}
170
- type="password"
171
- required
172
- />
173
- )}
174
- />
175
- <div className="actions">
176
- <HistoryBackButton
177
- content={<FormattedMessage id="actions.cancel" />}
178
- />
179
- <Button
180
- primary
181
- type="submit"
182
- disabled={!isValid}
183
- loading={passwordUpdating}
184
- >
185
- {formatMessage({ id: "user.password.save" })}
186
- </Button>
187
- </div>
188
- </Form>
189
- </Segment>
190
- </Container>
9
+ <UserPasswordModal
10
+ user={{ id, user_name: userName || authUserName }}
11
+ authUserName={authUserName}
12
+ onClose={() => navigate(-1)}
13
+ />
191
14
  );
192
15
  };
193
16
 
@@ -197,9 +20,8 @@ Password.propTypes = {
197
20
  authUserName: PropTypes.string,
198
21
  };
199
22
 
200
- const mapStateToProps = ({ authentication, passwordUpdating }) => ({
201
- passwordUpdating,
23
+ const mapStateToProps = ({ authentication }) => ({
202
24
  authUserName: authentication?.user_name,
203
25
  });
204
26
 
205
- export default connect(mapStateToProps, { updatePassword })(Password);
27
+ export default connect(mapStateToProps)(Password);
@@ -3,11 +3,28 @@ import PropTypes from "prop-types";
3
3
  import { connect } from "react-redux";
4
4
  import { Header, Table } from "semantic-ui-react";
5
5
  import { useIntl } from "react-intl";
6
+ import { DomainTooltip } from "@truedat/bg/taxonomy/components/DomainPath";
6
7
  import { getUserAcls } from "../selectors";
7
8
  import UserAclRow from "./UserAclRow";
8
9
 
9
- export const UserAcls = ({ acls }) => {
10
+ export const UserAcls = ({ acls, groupBy }) => {
10
11
  const { formatMessage } = useIntl();
12
+ const grouped = groupBy
13
+ ? Object.entries(
14
+ _.groupBy((acl) =>
15
+ groupBy === "role"
16
+ ? acl.role
17
+ : `${acl.resource?.type}:${acl.resource?.id ?? acl.resource?.name}`,
18
+ )(acls || []),
19
+ ).sort(([, left], [, right]) =>
20
+ (groupBy === "role"
21
+ ? left[0].role
22
+ : left[0].resource?.name || ""
23
+ ).localeCompare(
24
+ groupBy === "role" ? right[0].role : right[0].resource?.name || "",
25
+ ),
26
+ )
27
+ : [];
11
28
  return _.isEmpty(acls) ? null : (
12
29
  <>
13
30
  <Header as="h2" content={formatMessage({ id: "user.acl" })} />
@@ -15,17 +32,49 @@ export const UserAcls = ({ acls }) => {
15
32
  <Table.Header>
16
33
  <Table.Row>
17
34
  <Table.HeaderCell
18
- content={formatMessage({ id: "user.acl.domain" })}
35
+ content={formatMessage({
36
+ id: groupBy === "role" ? "user.acl.role" : "user.acl.domain",
37
+ })}
19
38
  />
20
39
  <Table.HeaderCell
21
- content={formatMessage({ id: "user.acl.role" })}
40
+ content={formatMessage({
41
+ id: groupBy === "role" ? "user.acl.domain" : "user.acl.role",
42
+ })}
22
43
  />
23
44
  </Table.Row>
24
45
  </Table.Header>
25
46
  <Table.Body>
26
- {acls.map((a, i) => (
27
- <UserAclRow key={i} {...a} />
28
- ))}
47
+ {groupBy
48
+ ? grouped.map(([key, entries]) => (
49
+ <Table.Row key={key}>
50
+ <Table.Cell>
51
+ {groupBy === "role" ? (
52
+ entries[0].role
53
+ ) : (
54
+ <DomainTooltip domain={entries[0].resource} />
55
+ )}
56
+ </Table.Cell>
57
+ <Table.Cell>
58
+ <ul className="um-acl-values">
59
+ {groupBy === "role"
60
+ ? _.uniqBy(
61
+ (acl) =>
62
+ `${acl.resource?.type}:${acl.resource?.id ?? acl.resource?.name}`,
63
+ )(entries).map(({ resource }) => (
64
+ <li
65
+ key={`${resource?.type}:${resource?.id ?? resource?.name}`}
66
+ >
67
+ <DomainTooltip domain={resource} />
68
+ </li>
69
+ ))
70
+ : _.uniq(entries.map(({ role }) => role))
71
+ .sort()
72
+ .map((role) => <li key={role}>{role}</li>)}
73
+ </ul>
74
+ </Table.Cell>
75
+ </Table.Row>
76
+ ))
77
+ : acls.map((a, i) => <UserAclRow key={i} {...a} />)}
29
78
  </Table.Body>
30
79
  </Table>
31
80
  </>
@@ -34,6 +83,7 @@ export const UserAcls = ({ acls }) => {
34
83
 
35
84
  UserAcls.propTypes = {
36
85
  acls: PropTypes.array,
86
+ groupBy: PropTypes.oneOf(["domain", "role"]),
37
87
  };
38
88
 
39
89
  const mapStateToProps = (state) => ({
@@ -1,45 +1,112 @@
1
1
  import _ from "lodash/fp";
2
- import { connect, useDispatch } from "react-redux";
2
+ import { connect } from "react-redux";
3
+ import { useState } from "react";
3
4
  import PropTypes from "prop-types";
4
5
  import { Button, Icon } from "semantic-ui-react";
5
- import { FormattedMessage } from "react-intl";
6
- import ConfirmModal from "@truedat/core/components/ConfirmModal";
6
+ import { FormattedMessage, useIntl } from "react-intl";
7
+ import TrueModal from "@truedat/core/components/TrueModal";
7
8
  import GroupActions from "@truedat/core/components/GroupActions";
8
9
  import { linkTo, USERS } from "@truedat/core/routes";
9
10
  import { Link, useNavigate } from "react-router";
10
- import { deleteUser, fetchUsers } from "../routines";
11
+ import { deleteUser } from "../routines";
12
+ import useUserModalOperation from "../hooks/useUserModalOperation";
11
13
 
12
- const ModalDelete = ({ handleDelete, id, name }) => (
13
- <ConfirmModal
14
- icon="trash"
15
- trigger={
14
+ export const DeleteUserModal = ({ id, name, onClose, onDeleted }) => {
15
+ const { formatMessage } = useIntl();
16
+ const {
17
+ submit,
18
+ isSubmitting,
19
+ onClose: closeModal,
20
+ error,
21
+ } = useUserModalOperation(deleteUser, onClose, () => {
22
+ onClose();
23
+ onDeleted?.();
24
+ });
25
+ return (
26
+ <TrueModal
27
+ open
28
+ title={formatMessage({ id: "user.delete.title" })}
29
+ headerColor="#ffffff"
30
+ headerTextColor="var(--td-danger, #b91c1c)"
31
+ icon="warning sign"
32
+ className="user-modal user-modal--delete"
33
+ size="mini"
34
+ onClose={closeModal}
35
+ aria-busy={isSubmitting}
36
+ closeOnEscape={!isSubmitting}
37
+ closeOnDimmerClick={!isSubmitting}
38
+ banner={error ? <span role="alert">{error}</span> : null}
39
+ bannerType="danger"
40
+ actions={[
41
+ {
42
+ key: "cancel",
43
+ secondary: true,
44
+ content: formatMessage({ id: "actions.cancel" }),
45
+ disabled: isSubmitting,
46
+ onClick: closeModal,
47
+ },
48
+ {
49
+ key: "delete",
50
+ negative: true,
51
+ content: formatMessage({ id: "user.delete.submit" }),
52
+ loading: isSubmitting,
53
+ onClick: () => submit({ id }),
54
+ },
55
+ ]}
56
+ >
57
+ <p>
58
+ <FormattedMessage
59
+ id="user.delete.description"
60
+ values={{
61
+ name,
62
+ strong: (chunks) => <strong>{chunks}</strong>,
63
+ danger: (chunks) => (
64
+ <span className="um-danger-text">{chunks}</span>
65
+ ),
66
+ }}
67
+ />
68
+ </p>
69
+ <p>
70
+ <FormattedMessage id="user.delete.confirm" />
71
+ </p>
72
+ </TrueModal>
73
+ );
74
+ };
75
+
76
+ DeleteUserModal.propTypes = {
77
+ id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
78
+ name: PropTypes.string,
79
+ onDeleted: PropTypes.func,
80
+ onClose: PropTypes.func.isRequired,
81
+ };
82
+
83
+ const ModalDelete = ({ id, name }) => {
84
+ const navigate = useNavigate();
85
+ const [open, setOpen] = useState(false);
86
+ return (
87
+ <>
16
88
  <Button
17
89
  icon={<Icon name="trash alternate outline" color="red" />}
18
90
  content={<FormattedMessage id="actions.delete" />}
91
+ onClick={() => setOpen(true)}
19
92
  />
20
- }
21
- header={<FormattedMessage id="user.actions.delete.confirmation.header" />}
22
- size="small"
23
- content={
24
- <FormattedMessage
25
- id="user.actions.delete.confirmation.content"
26
- values={{ name: <i>{name}</i> }}
27
- />
28
- }
29
- onConfirm={() => handleDelete({ id })}
30
- />
31
- );
32
-
33
- export const UserActions = ({ deleteUser, user: { id, name } }) => {
34
- const navigate = useNavigate();
35
- const dispatch = useDispatch();
36
-
37
- const onConfirmDelete = async ({ id }) => {
38
- await deleteUser({ id });
39
- dispatch(fetchUsers());
40
- navigate(USERS);
41
- };
93
+ {open && (
94
+ <DeleteUserModal
95
+ onDeleted={() => navigate(USERS)}
96
+ id={id}
97
+ name={name}
98
+ onClose={() => setOpen(false)}
99
+ />
100
+ )}
101
+ </>
102
+ );
103
+ };
104
+ ModalDelete.propTypes = {
105
+ id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
106
+ name: PropTypes.string,
107
+ };
42
108
 
109
+ export const UserActions = ({ user: { id, name, full_name, user_name } }) => {
43
110
  const generateAvailableActions = () => {
44
111
  const actions = {
45
112
  edit: true,
@@ -77,9 +144,8 @@ export const UserActions = ({ deleteUser, user: { id, name } }) => {
77
144
  icon: "trash",
78
145
  value: "delete",
79
146
  as: ModalDelete,
80
- handleDelete: onConfirmDelete,
81
147
  id,
82
- name,
148
+ name: full_name || name || user_name,
83
149
  selected: false,
84
150
  active: false,
85
151
  },
@@ -99,10 +165,9 @@ export const UserActions = ({ deleteUser, user: { id, name } }) => {
99
165
  };
100
166
 
101
167
  UserActions.propTypes = {
102
- deleteUser: PropTypes.func,
103
168
  user: PropTypes.object.isRequired,
104
169
  };
105
170
 
106
171
  const mapStateToProps = ({ user }) => ({ user });
107
172
 
108
- export default connect(mapStateToProps, { deleteUser })(UserActions);
173
+ export default connect(mapStateToProps)(UserActions);
@@ -1,32 +1,178 @@
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
+ content={
53
+ <div className="uc-groups-popup">
54
+ <div className="uc-groups-popup__header">
55
+ <Icon name="users" />
56
+ <span>{formatMessage({ id: "user.form.groups" })}</span>
57
+ </div>
58
+ <ul className="uc-groups-popup__list">
59
+ {groups.map((group) => {
60
+ const groupName = getGroupName(group);
61
+ return (
62
+ <li className="uc-groups-popup__item" key={groupName}>
63
+ <span className="uc-groups-popup__marker">
64
+ {groupName[0]?.toUpperCase()}
65
+ </span>
66
+ <span className="uc-groups-popup__name">{groupName}</span>
67
+ </li>
68
+ );
69
+ })}
70
+ </ul>
71
+ </div>
72
+ }
73
+ position="bottom center"
74
+ trigger={label}
75
+ />
76
+ );
77
+ };
78
+
79
+ GroupsBadge.propTypes = {
80
+ id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
81
+ groups: PropTypes.array,
82
+ };
83
+
84
+ export const UserCard = ({
85
+ user: { user_name, id, full_name, email, external_id, groups },
86
+ }) => {
87
+ const { formatMessage } = useIntl();
88
+ const userName = user_name?.trim() || "";
89
+ const fullName = full_name?.trim() || "";
90
+ const displayName = fullName || userName;
91
+ const editUserLabel = formatMessage({ id: "users.actions.edit" });
92
+ const editPasswordLabel = formatMessage({
93
+ id: "user.actions.edit_password",
94
+ });
95
+ const usernameDetail = fullName ? userName : "";
96
+ const userEmail = email?.trim() || "";
97
+ const externalId = external_id?.trim() || "";
98
+ const userGroups = groups || [];
99
+
100
+ return (
101
+ <Card key={id} className="td-card-base td-card--user">
102
+ <Card.Content
103
+ as={Link}
104
+ className="uc-card-link"
105
+ to={linkTo.USER({ id })}
106
+ aria-label={formatMessage(
107
+ { id: "user.info.open" },
108
+ { name: displayName },
109
+ )}
110
+ >
111
+ <div className="uc-summary">
112
+ <div className="uc-avatar">{getInitials(full_name, user_name)}</div>
113
+ <div className="uc-name" title={displayName}>
114
+ {displayName}
115
+ </div>
116
+ </div>
117
+
118
+ <div className="uc-meta-grid">
119
+ {usernameDetail && (
120
+ <>
121
+ <Icon name="user" />
122
+ <span className="uc-username">{usernameDetail}</span>
123
+ </>
124
+ )}
125
+ {userEmail && (
126
+ <>
127
+ <Icon name="mail" />
128
+ <span className="uc-email">{userEmail}</span>
129
+ </>
130
+ )}
131
+ {externalId && (
132
+ <>
133
+ <Icon name="id badge" />
134
+ <span
135
+ className="uc-external-id"
136
+ title={`${formatMessage({
137
+ id: "user.form.external_id",
138
+ defaultMessage: "ID externo",
139
+ })}: ${externalId}`}
140
+ >
141
+ {externalId}
142
+ </span>
143
+ </>
144
+ )}
145
+ </div>
146
+ </Card.Content>
147
+
148
+ <div className="uc-actions">
149
+ <Link
150
+ className="uc-action-btn"
151
+ to={linkTo.USER_EDIT({ id })}
152
+ aria-label={editUserLabel}
153
+ onClick={(e) => e.stopPropagation()}
154
+ >
155
+ <Icon name="pencil alternate" />
156
+ <span className="uc-action-label">{editUserLabel}</span>
157
+ </Link>
158
+ <Link
159
+ className="uc-action-btn uc-action-btn--key"
160
+ to={linkTo.USER_EDIT_PASSWORD({ id })}
161
+ aria-label={editPasswordLabel}
162
+ onClick={(e) => e.stopPropagation()}
163
+ >
164
+ <Icon name="key" />
165
+ <span className="uc-action-label">{editPasswordLabel}</span>
166
+ </Link>
167
+ </div>
168
+
169
+ <GroupsBadge groups={userGroups} id={id} />
170
+ </Card>
171
+ );
172
+ };
173
+
27
174
  UserCard.propTypes = {
28
175
  user: PropTypes.object,
29
- showFullName: PropTypes.bool,
30
176
  };
31
177
 
32
- export default connect(null, { deleteUser })(UserCard);
178
+ export default UserCard;