@truedat/auth 8.14.1 → 8.14.2

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 (65) hide show
  1. package/package.json +4 -4
  2. package/src/groups/components/EditGroup.js +64 -20
  3. package/src/groups/components/Group.js +39 -41
  4. package/src/groups/components/GroupCard.js +51 -54
  5. package/src/groups/components/GroupCards.js +60 -25
  6. package/src/groups/components/GroupDeleteModal.js +98 -0
  7. package/src/groups/components/GroupDropdown.js +92 -0
  8. package/src/groups/components/GroupForm.js +165 -84
  9. package/src/groups/components/GroupFormModal.js +137 -0
  10. package/src/groups/components/GroupInfoModal.js +114 -0
  11. package/src/groups/components/GroupMemberPicker.js +364 -0
  12. package/src/groups/components/GroupRouteModal.js +46 -0
  13. package/src/groups/components/GroupUsers.js +31 -14
  14. package/src/groups/components/GroupUsersTable.js +136 -83
  15. package/src/groups/components/NewGroup.js +38 -10
  16. package/src/groups/components/__tests__/EditGroup.spec.js +105 -0
  17. package/src/groups/components/__tests__/Group.spec.js +29 -47
  18. package/src/groups/components/__tests__/GroupCard.spec.js +8 -20
  19. package/src/groups/components/__tests__/GroupDropdown.spec.js +27 -0
  20. package/src/groups/components/__tests__/GroupForm.spec.js +32 -5
  21. package/src/groups/components/__tests__/GroupMemberPicker.spec.js +378 -0
  22. package/src/groups/components/__tests__/GroupModals.spec.js +210 -0
  23. package/src/groups/components/__tests__/GroupUsers.spec.js +17 -2
  24. package/src/groups/components/__tests__/GroupUsersTable.spec.js +34 -2
  25. package/src/groups/components/__tests__/__snapshots__/Group.spec.js.snap +1 -0
  26. package/src/groups/components/__tests__/__snapshots__/GroupCard.spec.js.snap +32 -30
  27. package/src/groups/components/__tests__/__snapshots__/GroupCards.spec.js.snap +90 -77
  28. package/src/groups/components/__tests__/__snapshots__/GroupUsers.spec.js.snap +75 -11
  29. package/src/groups/components/__tests__/__snapshots__/GroupUsersTable.spec.js.snap +70 -13
  30. package/src/groups/components/index.js +0 -4
  31. package/src/groups/hooks/__tests__/useGroupOperation.spec.js +167 -0
  32. package/src/groups/hooks/__tests__/useGroups.spec.js +90 -0
  33. package/src/groups/hooks/useGroupOperation.js +83 -0
  34. package/src/groups/hooks/useGroups.js +64 -0
  35. package/src/groups/reducers/index.js +0 -4
  36. package/src/groups/routines.js +0 -7
  37. package/src/groups/sagas/index.js +2 -24
  38. package/src/hooks/useRoles.js +6 -3
  39. package/src/styles/Groups.less +1009 -0
  40. package/src/users/components/AdminUserRoutes.js +1 -14
  41. package/src/users/components/GroupUserCrumbs.js +7 -12
  42. package/src/users/components/UserRoutes.js +12 -35
  43. package/src/users/components/UsersAndGroups.js +3 -6
  44. package/src/users/components/__tests__/GroupUserCrumbs.spec.js +26 -30
  45. package/src/users/components/__tests__/UserRoutes.spec.js +4 -6
  46. package/src/users/components/__tests__/UsersAndGroups.spec.js +5 -7
  47. package/src/users/components/__tests__/__snapshots__/UserRoutes.spec.js.snap +9 -55
  48. package/src/groups/components/GroupLoader.js +0 -27
  49. package/src/groups/components/GroupsLoader.js +0 -30
  50. package/src/groups/components/__tests__/GroupLoader.spec.js +0 -71
  51. package/src/groups/components/__tests__/__snapshots__/GroupLoader.spec.js.snap +0 -11
  52. package/src/groups/reducers/__tests__/groupRedirect.spec.js +0 -51
  53. package/src/groups/reducers/__tests__/groupUsers.spec.js +0 -60
  54. package/src/groups/reducers/group.js +0 -56
  55. package/src/groups/reducers/groupRedirect.js +0 -20
  56. package/src/groups/reducers/groupUsers.js +0 -16
  57. package/src/groups/reducers/groups.js +0 -29
  58. package/src/groups/sagas/__tests__/deleteGroup.spec.js +0 -93
  59. package/src/groups/sagas/__tests__/deleteGroupUser.spec.js +0 -80
  60. package/src/groups/sagas/createGroup.js +0 -27
  61. package/src/groups/sagas/deleteGroup.js +0 -31
  62. package/src/groups/sagas/deleteGroupUser.js +0 -35
  63. package/src/groups/sagas/fetchGroup.js +0 -30
  64. package/src/groups/sagas/fetchGroups.js +0 -26
  65. package/src/groups/sagas/updateGroup.js +0 -28
@@ -0,0 +1,364 @@
1
+ import { useEffect, useMemo, useRef, useState } from "react";
2
+ import PropTypes from "prop-types";
3
+ import { useIntl } from "react-intl";
4
+ import { Button, Icon, Popup } from "semantic-ui-react";
5
+ import { useQuery } from "@apollo/client";
6
+ import { DOMAINS_QUERY } from "@truedat/core/api/queries";
7
+ import { useRoles } from "../../hooks/useRoles";
8
+ import { useFilteredGroupUsers } from "../hooks/useGroups";
9
+ import GroupDropdown from "./GroupDropdown";
10
+
11
+ const typeIcons = {
12
+ user: "user",
13
+ admin: "shield alternate",
14
+ service: "cogs",
15
+ agent: "microchip",
16
+ };
17
+
18
+ const GroupMemberPicker = ({
19
+ users,
20
+ options,
21
+ value,
22
+ onChange,
23
+ onBlur,
24
+ disabled,
25
+ showFilters = true,
26
+ }) => {
27
+ const { formatMessage } = useIntl();
28
+ const message = (id, defaultMessage) => formatMessage({ id, defaultMessage });
29
+ const selectedListRef = useRef(null);
30
+ const [addedIds, setAddedIds] = useState([]);
31
+ const addedTimerRef = useRef(null);
32
+ useEffect(() => () => clearTimeout(addedTimerRef.current), []);
33
+ const [searchQuery, setSearchQuery] = useState("");
34
+ const [type, setType] = useState("");
35
+ const [role, setRole] = useState("");
36
+ const [domains, setDomains] = useState([]);
37
+ const {
38
+ data: roles = [],
39
+ error: rolesError,
40
+ loading: rolesLoading,
41
+ retry: retryRoles,
42
+ } = useRoles(showFilters);
43
+ const {
44
+ data: domainData,
45
+ loading: domainsLoading,
46
+ error: domainsError,
47
+ refetch: retryDomains,
48
+ } = useQuery(DOMAINS_QUERY, {
49
+ variables: { action: "viewDomain" },
50
+ skip: !showFilters,
51
+ });
52
+ const domainOptions = useMemo(() => {
53
+ const domains = domainData?.domains || [];
54
+ const names = new Map(domains.map(({ id, name }) => [String(id), name]));
55
+ return domains
56
+ .map(({ id, name, parentId }) => ({
57
+ key: id,
58
+ value: String(id),
59
+ text: names.has(String(parentId))
60
+ ? `${names.get(String(parentId))} / ${name}`
61
+ : name,
62
+ }))
63
+ .sort((a, b) => a.text.localeCompare(b.text));
64
+ }, [domainData]);
65
+ const {
66
+ users: result,
67
+ error: searchError,
68
+ loading: searchLoading,
69
+ retry: retrySearch,
70
+ } = useFilteredGroupUsers(showFilters, role, domains);
71
+ const loading = rolesLoading || searchLoading;
72
+ const failed = Boolean(rolesError || searchError);
73
+ const usersById = useMemo(
74
+ () => new Map(users.map((user) => [user.id, user])),
75
+ [users],
76
+ );
77
+ const filteredOptions = useMemo(() => {
78
+ const selected = new Set(value);
79
+ const resultIds =
80
+ showFilters && result && new Set(result.map(({ id }) => String(id)));
81
+ const matches = new Set(
82
+ users
83
+ .filter(
84
+ (user) =>
85
+ (!resultIds || resultIds.has(String(user.id))) &&
86
+ (!showFilters || !type || user.role === type),
87
+ )
88
+ .map(({ id }) => id),
89
+ );
90
+ return options.filter(
91
+ ({ value: id }) => !selected.has(id) && matches.has(id),
92
+ );
93
+ }, [options, result, users, type, value, showFilters]);
94
+ const addUsers = (ids) => {
95
+ if (disabled || (showFilters && (loading || failed))) return;
96
+ const added = ids.filter((id) => !value.includes(id));
97
+ if (!added.length) return;
98
+ clearTimeout(addedTimerRef.current);
99
+ setAddedIds(added);
100
+ addedTimerRef.current = setTimeout(() => setAddedIds([]), 900);
101
+ onChange([...added, ...value]);
102
+ setSearchQuery("");
103
+ if (selectedListRef.current) selectedListRef.current.scrollTop = 0;
104
+ };
105
+ const canAddAll =
106
+ showFilters &&
107
+ Boolean(role || domains.length) &&
108
+ filteredOptions.length > 1;
109
+ return (
110
+ <div
111
+ className={`group-member-picker${showFilters ? "" : " group-member-picker--without-filters"}`}
112
+ >
113
+ {showFilters && (
114
+ <aside
115
+ className="group-member-filters"
116
+ aria-label={message("group.members.filters", "Filtros de miembros")}
117
+ >
118
+ <GroupDropdown
119
+ selection
120
+ placeholder={message("group.members.all", "Todos")}
121
+ label={message("group.members.type", "Tipo de usuario")}
122
+ aria-label={message("group.members.type", "Tipo de usuario")}
123
+ value={type}
124
+ disabled={disabled}
125
+ options={[
126
+ {
127
+ key: "all",
128
+ value: "",
129
+ text: message("group.members.all", "Todos"),
130
+ },
131
+ ...["user", "admin", "service", "agent"].map((item) => ({
132
+ key: item,
133
+ value: item,
134
+ text: message(
135
+ item === "agent"
136
+ ? "group.members.agent"
137
+ : `user.type.${item}`,
138
+ item === "agent" ? "Agente" : undefined,
139
+ ),
140
+ })),
141
+ ]}
142
+ onChange={(_event, { value }) => setType(value)}
143
+ />
144
+ <GroupDropdown
145
+ selection
146
+ search
147
+ placeholder={message("group.members.all", "Todos")}
148
+ label={message("group.members.role", "Rol")}
149
+ aria-label={message("group.members.role", "Rol")}
150
+ value={role}
151
+ disabled={disabled}
152
+ options={[
153
+ {
154
+ key: "all",
155
+ value: "",
156
+ text: message("group.members.all", "Todos"),
157
+ },
158
+ ...roles.map(({ id, name }) => ({
159
+ key: id,
160
+ value: String(id),
161
+ text: name,
162
+ })),
163
+ ]}
164
+ onChange={(_event, { value }) => setRole(value)}
165
+ />
166
+ <GroupDropdown
167
+ selection
168
+ search
169
+ multiple
170
+ label={message("group.members.domain", "Dominio")}
171
+ aria-label={message("group.members.domain", "Dominio")}
172
+ placeholder={message("domain.multiple.placeholder")}
173
+ value={domains}
174
+ options={domainOptions}
175
+ disabled={disabled || domainsLoading || Boolean(domainsError)}
176
+ loading={domainsLoading}
177
+ error={Boolean(domainsError)}
178
+ onChange={(_event, { value }) => setDomains(value)}
179
+ />
180
+ {domainsError && (
181
+ <div role="alert">
182
+ <p>
183
+ {message(
184
+ "group.members.domainsError",
185
+ "No se han podido cargar los dominios.",
186
+ )}
187
+ </p>
188
+ <Button
189
+ type="button"
190
+ size="small"
191
+ disabled={disabled || domainsLoading}
192
+ onClick={() => retryDomains().catch(() => {})}
193
+ >
194
+ {message("group.members.retry", "Reintentar")}
195
+ </Button>
196
+ </div>
197
+ )}
198
+ </aside>
199
+ )}
200
+ <div className="group-member-selection">
201
+ <div className="group-add-control">
202
+ <GroupDropdown
203
+ closeOnChange
204
+ selectOnBlur={false}
205
+ name="user_ids"
206
+ aria-label={message("group.members.add", "Añadir miembros")}
207
+ label={message("group.members.add", "Añadir miembros")}
208
+ search
209
+ selection
210
+ options={filteredOptions}
211
+ value=""
212
+ searchQuery={searchQuery}
213
+ onSearchChange={(_event, { searchQuery }) =>
214
+ setSearchQuery(searchQuery)
215
+ }
216
+ onBlur={onBlur}
217
+ disabled={disabled || (showFilters && (loading || failed))}
218
+ loading={showFilters && loading}
219
+ onChange={(_event, { value: id }) => addUsers([id])}
220
+ placeholder={message("group.form.users.placeholder")}
221
+ />
222
+ {canAddAll && (
223
+ <Button
224
+ type="button"
225
+ size="mini"
226
+ className="group-add-all"
227
+ disabled={disabled || loading || failed}
228
+ onClick={() =>
229
+ addUsers(filteredOptions.map(({ value: id }) => id))
230
+ }
231
+ >
232
+ {message("group.members.addAll", "Añadir todos")}
233
+ </Button>
234
+ )}
235
+ </div>
236
+ <section
237
+ className="group-selected-members"
238
+ aria-label={message(
239
+ "group.members.selected",
240
+ "Miembros seleccionados",
241
+ )}
242
+ >
243
+ <h4>
244
+ {message("group.members.selected", "Miembros seleccionados")}{" "}
245
+ <span>({value.length})</span>
246
+ </h4>
247
+ {value.length ? (
248
+ <ul ref={selectedListRef}>
249
+ {value.map((id) => {
250
+ const user = usersById.get(id);
251
+ const name = user?.full_name || user?.user_name || String(id);
252
+ const parts = name.trim().split(/\s+/);
253
+ const initials =
254
+ (parts[0]?.[0] || "?") +
255
+ (parts.length > 1 ? parts[parts.length - 1][0] : "");
256
+ const typeLabel = user?.role
257
+ ? message(
258
+ user.role === "agent"
259
+ ? "group.members.agent"
260
+ : `user.type.${user.role}`,
261
+ user.role === "agent" ? "Agente" : undefined,
262
+ )
263
+ : null;
264
+ return (
265
+ <li
266
+ key={id}
267
+ className={
268
+ addedIds.includes(id) ? "group-member-added" : undefined
269
+ }
270
+ >
271
+ <span className="group-member-identity">
272
+ <span className="group-member-avatar" aria-hidden="true">
273
+ {initials.toUpperCase()}
274
+ </span>
275
+ <span className="group-member-details">
276
+ <span className="group-member-name">
277
+ <strong>{name}</strong>
278
+ {typeLabel && (
279
+ <Popup
280
+ inverted
281
+ size="mini"
282
+ content={typeLabel}
283
+ on={["hover", "focus"]}
284
+ position="top center"
285
+ trigger={
286
+ <span
287
+ className="group-member-type"
288
+ tabIndex={0}
289
+ aria-label={typeLabel}
290
+ >
291
+ <Icon
292
+ name={typeIcons[user.role] || "user"}
293
+ aria-hidden="true"
294
+ />
295
+ </span>
296
+ }
297
+ />
298
+ )}
299
+ </span>
300
+ {user?.full_name && user?.user_name && (
301
+ <small>{user.user_name}</small>
302
+ )}
303
+ </span>
304
+ </span>
305
+ <Button
306
+ type="button"
307
+ icon
308
+ basic
309
+ disabled={disabled}
310
+ aria-label={formatMessage(
311
+ {
312
+ id: "group.members.remove",
313
+ defaultMessage: "Eliminar a {name} del grupo",
314
+ },
315
+ { name },
316
+ )}
317
+ onClick={() =>
318
+ onChange(value.filter((memberId) => memberId !== id))
319
+ }
320
+ >
321
+ <Icon name="times" />
322
+ </Button>
323
+ </li>
324
+ );
325
+ })}
326
+ </ul>
327
+ ) : (
328
+ <p>
329
+ {message("group.members.empty", "No hay miembros seleccionados")}
330
+ </p>
331
+ )}
332
+ </section>
333
+ {showFilters && failed && (
334
+ <div role="alert">
335
+ <p>
336
+ {message(
337
+ "group.members.error",
338
+ "No se han podido cargar los usuarios.",
339
+ )}
340
+ </p>
341
+ <Button
342
+ type="button"
343
+ size="small"
344
+ disabled={disabled}
345
+ onClick={() => Promise.allSettled([retryRoles(), retrySearch()])}
346
+ >
347
+ {message("group.members.retry", "Reintentar")}
348
+ </Button>
349
+ </div>
350
+ )}
351
+ </div>
352
+ </div>
353
+ );
354
+ };
355
+ GroupMemberPicker.propTypes = {
356
+ users: PropTypes.array.isRequired,
357
+ options: PropTypes.array.isRequired,
358
+ value: PropTypes.array.isRequired,
359
+ onChange: PropTypes.func.isRequired,
360
+ onBlur: PropTypes.func,
361
+ disabled: PropTypes.bool,
362
+ showFilters: PropTypes.bool,
363
+ };
364
+ export default GroupMemberPicker;
@@ -0,0 +1,46 @@
1
+ import PropTypes from "prop-types";
2
+ import { useCallback, useEffect } from "react";
3
+ import { useNavigate, useLocation, useParams } from "react-router";
4
+ import { GROUPS } from "@truedat/core/routes";
5
+ import GroupFormModal from "./GroupFormModal";
6
+ import GroupInfoModal from "./GroupInfoModal";
7
+
8
+ const GroupRouteModal = ({ mode }) => {
9
+ const navigate = useNavigate();
10
+ const { pathname } = useLocation();
11
+ const { id } = useParams();
12
+ useEffect(() => {
13
+ const opener = document.activeElement;
14
+ return () => {
15
+ requestAnimationFrame(() => {
16
+ if (document.querySelector(".group-modal")) return;
17
+ if (opener?.isConnected && opener !== document.body) opener.focus();
18
+ else
19
+ document
20
+ .querySelector(`a[href="${pathname.replace(/\/edit$/, "")}"]`)
21
+ ?.focus();
22
+ });
23
+ };
24
+ }, [pathname]);
25
+ const onClose = useCallback(() => {
26
+ navigate(GROUPS);
27
+ requestAnimationFrame(() => {
28
+ document
29
+ .querySelector(`a[href="${pathname.replace(/\/edit$/, "")}"]`)
30
+ ?.focus();
31
+ });
32
+ }, [navigate, pathname]);
33
+ return mode === "info" ? (
34
+ <GroupInfoModal key={id} onClose={onClose} />
35
+ ) : (
36
+ <GroupFormModal
37
+ key={id || "new"}
38
+ editing={mode === "edit"}
39
+ onClose={onClose}
40
+ />
41
+ );
42
+ };
43
+ GroupRouteModal.propTypes = {
44
+ mode: PropTypes.oneOf(["new", "edit", "info"]).isRequired,
45
+ };
46
+ export default GroupRouteModal;
@@ -1,19 +1,38 @@
1
1
  import _ from "lodash/fp";
2
2
  import { useState } from "react";
3
- import PropTypes from "prop-types";
4
- import { Link } from "react-router";
5
- import { connect } from "react-redux";
3
+ import { Link, useParams } from "react-router";
6
4
  import { Segment, Button, Divider } from "semantic-ui-react";
7
5
  import { FormattedMessage, useIntl } from "react-intl";
8
6
  import SearchInput from "@truedat/core/components/SearchInput";
9
7
  import { linkTo } from "@truedat/core/routes";
8
+ import { useGroup, useGroupUsers } from "../hooks/useGroups";
10
9
  import GroupUsersTable from "./GroupUsersTable";
11
10
 
12
- export const GroupUsers = ({ group, groupLoading }) => {
11
+ export const GroupUsers = () => {
12
+ const { id } = useParams();
13
+ const {
14
+ group,
15
+ members: groupUsers,
16
+ loading: groupLoading,
17
+ error,
18
+ retry,
19
+ } = useGroup(id);
13
20
  const [filter, setFilter] = useState("");
14
21
  const { formatMessage } = useIntl();
22
+ const { users: directoryUsers } = useGroupUsers();
15
23
 
16
- return groupLoading || _.isEmpty(group) ? null : (
24
+ if (groupLoading) return <p role="status">Cargando grupo…</p>;
25
+ if (error || _.isEmpty(group)) {
26
+ return (
27
+ <div role="alert">
28
+ No se ha podido cargar el grupo.
29
+ <button type="button" onClick={() => retry().catch(() => {})}>
30
+ Reintentar
31
+ </button>
32
+ </div>
33
+ );
34
+ }
35
+ return (
17
36
  <Segment attached="bottom">
18
37
  <Button
19
38
  floated="right"
@@ -28,16 +47,14 @@ export const GroupUsers = ({ group, groupLoading }) => {
28
47
  value={filter}
29
48
  />
30
49
  <Divider hidden />
31
- <GroupUsersTable filter={filter} />
50
+ <GroupUsersTable
51
+ filter={filter}
52
+ group={group}
53
+ groupUsers={groupUsers}
54
+ users={directoryUsers}
55
+ />
32
56
  </Segment>
33
57
  );
34
58
  };
35
59
 
36
- GroupUsers.propTypes = {
37
- group: PropTypes.object,
38
- groupLoading: PropTypes.bool,
39
- };
40
-
41
- const mapStateToProps = ({ group, groupLoading }) => ({ group, groupLoading });
42
-
43
- export default connect(mapStateToProps)(GroupUsers);
60
+ export default GroupUsers;