@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
@@ -1,11 +1,12 @@
1
1
  import _ from "lodash/fp";
2
+ import { useId, useMemo } from "react";
2
3
  import PropTypes from "prop-types";
3
4
  import { useForm, Controller } from "react-hook-form";
4
5
  import { useIntl } from "react-intl";
5
- import { connect } from "react-redux";
6
6
  import { Button, Form, Header } from "semantic-ui-react";
7
7
  import HistoryBackButton from "@truedat/core/components/HistoryBackButton";
8
8
  import { accentInsensitivePathOrder } from "@truedat/core/services/sort";
9
+ import GroupMemberPicker from "./GroupMemberPicker";
9
10
 
10
11
  const toOption = ({ id, full_name, user_name }) => ({
11
12
  key: id,
@@ -26,8 +27,12 @@ export const GroupForm = ({
26
27
  isSubmitting,
27
28
  users,
28
29
  onSubmit,
30
+ onCancel,
31
+ onDelete,
32
+ memberFilters = false,
29
33
  }) => {
30
34
  const { formatMessage } = useIntl();
35
+ const nameFieldId = useId();
31
36
  const { handleSubmit, control, formState } = useForm({
32
37
  mode: "all",
33
38
  defaultValues: {
@@ -38,96 +43,174 @@ export const GroupForm = ({
38
43
  },
39
44
  });
40
45
  const { errors, isDirty, isValid } = formState;
46
+ const DescriptionField = memberFilters ? Form.Input : Form.TextArea;
47
+ const options = useMemo(() => userOptions(users || []), [users]);
41
48
  return (
42
- <Form onSubmit={handleSubmit(onSubmit)}>
43
- <Controller
44
- control={control}
45
- name="name"
46
- rules={{
47
- required: formatMessage(
48
- { id: "form.validation.required" },
49
- { prop: formatMessage({ id: "group.props.name" }) },
50
- ),
51
- }}
52
- render={({ field: { onBlur, onChange, value } }) => (
53
- <Form.Input
54
- autoComplete="off"
55
- error={_.path(["name", "message"])(errors)}
56
- label={formatMessage({ id: "group.props.name" })}
57
- onBlur={onBlur}
58
- onChange={(_e, { value }) => onChange(value)}
59
- placeholder={formatMessage({
60
- id: "group.form.name.placeholder",
61
- })}
62
- value={value || ""}
63
- required
64
- />
65
- )}
66
- />
67
- <Controller
68
- control={control}
69
- name="description"
70
- render={({ field: { onBlur, onChange, value } }) => (
71
- <Form.Input
72
- autoComplete="off"
73
- label={formatMessage({ id: "group.props.description" })}
74
- onBlur={onBlur}
75
- onChange={(_e, { value }) => onChange(value)}
76
- placeholder={formatMessage({
77
- id: "group.form.description.placeholder",
78
- })}
79
- value={value || ""}
49
+ <Form
50
+ className="group-form"
51
+ onSubmit={handleSubmit((data) => !isSubmitting && onSubmit(data))}
52
+ >
53
+ <fieldset disabled={isSubmitting} className="group-form-fields">
54
+ <Controller
55
+ control={control}
56
+ name="name"
57
+ rules={{
58
+ required: formatMessage(
59
+ { id: "form.validation.required" },
60
+ { prop: formatMessage({ id: "group.props.name" }) },
61
+ ),
62
+ }}
63
+ render={({ field: { onBlur, onChange, value } }) => (
64
+ <Form.Input
65
+ id={nameFieldId}
66
+ className={
67
+ memberFilters
68
+ ? `group-name-field${value ? " group-name-field--filled" : ""}`
69
+ : undefined
70
+ }
71
+ autoComplete="off"
72
+ error={_.path(["name", "message"])(errors)}
73
+ label={formatMessage(
74
+ memberFilters
75
+ ? {
76
+ id: "group.form.name.label",
77
+ defaultMessage: "Nombre del grupo",
78
+ }
79
+ : { id: "group.props.name" },
80
+ )}
81
+ onBlur={onBlur}
82
+ onChange={(_e, { value }) => onChange(value)}
83
+ placeholder={formatMessage({
84
+ id: "group.form.name.placeholder",
85
+ })}
86
+ value={value || ""}
87
+ required
88
+ />
89
+ )}
90
+ />
91
+ <Controller
92
+ control={control}
93
+ name="alias"
94
+ render={({ field: { onBlur, onChange, value } }) => (
95
+ <Form.Input
96
+ autoComplete="off"
97
+ label={formatMessage({ id: "group.props.alias" })}
98
+ onBlur={onBlur}
99
+ onChange={(_e, { value }) => onChange(value)}
100
+ placeholder={formatMessage({
101
+ id: "group.form.alias.placeholder",
102
+ })}
103
+ value={value || ""}
104
+ />
105
+ )}
106
+ />
107
+ <Controller
108
+ control={control}
109
+ name="description"
110
+ rules={
111
+ memberFilters
112
+ ? {
113
+ maxLength: {
114
+ value: 55,
115
+ message: formatMessage({
116
+ id: "group.description.maxLength",
117
+ defaultMessage:
118
+ "La descripción no puede superar los 55 caracteres.",
119
+ }),
120
+ },
121
+ }
122
+ : undefined
123
+ }
124
+ render={({ field: { onBlur, onChange, value } }) => (
125
+ <DescriptionField
126
+ className="group-form-description"
127
+ {...(memberFilters ? { maxLength: 55 } : { rows: 2 })}
128
+ error={errors.description?.message}
129
+ label={formatMessage({
130
+ id: memberFilters
131
+ ? "group.description.short"
132
+ : "group.props.description",
133
+ defaultMessage: memberFilters
134
+ ? "Descripción breve"
135
+ : "Descripción",
136
+ })}
137
+ onBlur={onBlur}
138
+ onChange={(_e, { value }) => onChange(value)}
139
+ placeholder={formatMessage({
140
+ id: "group.form.description.placeholder",
141
+ defaultMessage: "Descripción del grupo",
142
+ })}
143
+ value={value || ""}
144
+ />
145
+ )}
146
+ />
147
+ <Controller
148
+ control={control}
149
+ name="user_ids"
150
+ render={({ field: { onBlur, onChange, value } }) =>
151
+ memberFilters ? (
152
+ <GroupMemberPicker
153
+ showFilters={false}
154
+ users={users || []}
155
+ options={options}
156
+ value={value || []}
157
+ onChange={onChange}
158
+ onBlur={onBlur}
159
+ disabled={isSubmitting}
160
+ />
161
+ ) : (
162
+ <Form.Dropdown
163
+ disabled={isSubmitting}
164
+ name="user_ids"
165
+ label={formatMessage({ id: "group.props.users" })}
166
+ search
167
+ selection
168
+ clearable
169
+ multiple
170
+ options={options}
171
+ onBlur={onBlur}
172
+ onChange={(_e, { value }) => onChange(value)}
173
+ placeholder={formatMessage({
174
+ id: "group.form.users.placeholder",
175
+ })}
176
+ value={value || []}
177
+ />
178
+ )
179
+ }
180
+ />
181
+ </fieldset>
182
+ <div className="actions">
183
+ {onDelete && (
184
+ <Button
185
+ type="button"
186
+ className="group-delete-action"
187
+ icon="trash alternate outline"
188
+ content={formatMessage({ id: "actions.delete" })}
189
+ disabled={isSubmitting}
190
+ onClick={onDelete}
80
191
  />
81
192
  )}
82
- />
83
- <Controller
84
- control={control}
85
- name="alias"
86
- render={({ field: { onBlur, onChange, value } }) => (
87
- <Form.Input
88
- autoComplete="off"
89
- label={formatMessage({ id: "group.props.alias" })}
90
- onBlur={onBlur}
91
- onChange={(_e, { value }) => onChange(value)}
92
- placeholder={formatMessage({
93
- id: "group.form.alias.placeholder",
94
- })}
95
- value={value || ""}
193
+ {onCancel ? (
194
+ <Button
195
+ type="button"
196
+ secondary
197
+ disabled={isSubmitting}
198
+ onClick={onCancel}
199
+ content={formatMessage({ id: "actions.cancel" })}
96
200
  />
97
- )}
98
- />
99
- <Controller
100
- control={control}
101
- name="user_ids"
102
- render={({ field: { onBlur, onChange, value } }) => (
103
- <Form.Dropdown
104
- name="user_ids"
105
- label={formatMessage({ id: "group.props.users" })}
106
- search
107
- selection
108
- clearable
109
- multiple
110
- options={userOptions(users)}
111
- onBlur={onBlur}
112
- onChange={(_e, { value }) => onChange(value)}
113
- placeholder={formatMessage({ id: "group.form.users.placeholder" })}
114
- value={value || []}
201
+ ) : (
202
+ <HistoryBackButton
203
+ content={formatMessage({ id: "actions.cancel" })}
204
+ disabled={isSubmitting}
115
205
  />
116
206
  )}
117
- />
118
- <div className="actions">
119
207
  <Button
120
- floated="right"
121
208
  type="submit"
122
209
  primary
123
210
  loading={isSubmitting}
124
211
  disabled={isSubmitting || !isDirty || !isValid}
125
212
  content={formatMessage({ id: "actions.save" })}
126
213
  />
127
- <HistoryBackButton
128
- content={formatMessage({ id: "actions.cancel" })}
129
- disabled={isSubmitting}
130
- />
131
214
  </div>
132
215
  </Form>
133
216
  );
@@ -135,15 +218,13 @@ export const GroupForm = ({
135
218
 
136
219
  GroupForm.propTypes = {
137
220
  users: PropTypes.array,
221
+ memberFilters: PropTypes.bool,
138
222
  group: PropTypes.object,
139
223
  groupUsers: PropTypes.array,
140
224
  onSubmit: PropTypes.func,
141
225
  isSubmitting: PropTypes.bool,
226
+ onCancel: PropTypes.func,
227
+ onDelete: PropTypes.func,
142
228
  };
143
229
 
144
- const mapStateToProps = ({ groupCreating: isSubmitting, users }) => ({
145
- isSubmitting,
146
- users,
147
- });
148
-
149
- export default connect(mapStateToProps)(GroupForm);
230
+ export default GroupForm;
@@ -0,0 +1,137 @@
1
+ import { useState } from "react";
2
+ import PropTypes from "prop-types";
3
+ import { useIntl } from "react-intl";
4
+ import { useParams } from "react-router";
5
+ import { TrueModal } from "@truedat/core/components/TrueModal";
6
+ import { useGroup, useGroupUsers } from "../hooks/useGroups";
7
+ import { useGroupOperation } from "../hooks/useGroupOperation";
8
+ import GroupForm from "./GroupForm";
9
+ import GroupDeleteModal from "./GroupDeleteModal";
10
+
11
+ const GroupFormModal = ({ editing = false, onClose }) => {
12
+ const [deleting, setDeleting] = useState(null);
13
+ const { formatMessage } = useIntl();
14
+ const { id } = useParams();
15
+ const {
16
+ group,
17
+ members: groupUsers,
18
+ hasData: hasGroup,
19
+ loading,
20
+ error,
21
+ retry,
22
+ } = useGroup(editing ? id : null);
23
+ const {
24
+ users,
25
+ hasData: hasUsers,
26
+ loading: usersLoading,
27
+ error: usersError,
28
+ retry: retryUsers,
29
+ } = useGroupUsers();
30
+ const { submit, pending, failed } = useGroupOperation(
31
+ editing ? "update" : "create",
32
+ id,
33
+ onClose,
34
+ );
35
+ const ready =
36
+ hasUsers &&
37
+ (!editing || (hasGroup && Boolean(group?.id) && String(group.id) === id));
38
+ return (
39
+ <>
40
+ <TrueModal
41
+ open
42
+ size="small"
43
+ className={`group-modal group-modal--form${editing ? " group-modal--edit" : " group-modal--new"}`}
44
+ title={
45
+ editing
46
+ ? formatMessage(
47
+ {
48
+ id: "group.edit.title",
49
+ defaultMessage: "Editar grupo {name}",
50
+ },
51
+ { name: group?.name || "" },
52
+ )
53
+ : formatMessage({ id: "groups.actions.create" })
54
+ }
55
+ icon="users"
56
+ onClose={() => !pending && !deleting && onClose()}
57
+ closeDisabled={pending || deleting}
58
+ closeOnEscape={!pending && !deleting}
59
+ closeOnDimmerClick={!pending && !deleting}
60
+ banner={
61
+ failed
62
+ ? formatMessage({
63
+ id: "group.operation.error",
64
+ defaultMessage:
65
+ "No se ha podido completar la operación. Inténtalo de nuevo.",
66
+ })
67
+ : null
68
+ }
69
+ bannerType="danger"
70
+ >
71
+ {ready ? (
72
+ <GroupForm
73
+ key={editing ? group.id : "new"}
74
+ memberFilters
75
+ group={editing ? group : {}}
76
+ groupUsers={editing ? groupUsers : []}
77
+ users={users}
78
+ isSubmitting={pending || deleting}
79
+ onDelete={editing ? () => setDeleting(group) : undefined}
80
+ onCancel={onClose}
81
+ onSubmit={(values) =>
82
+ submit(editing ? { ...values, id: group.id } : values)
83
+ }
84
+ />
85
+ ) : (
86
+ <p role="status">
87
+ {formatMessage({
88
+ id:
89
+ loading || usersLoading ? "group.loading" : "group.unavailable",
90
+ defaultMessage:
91
+ loading || usersLoading
92
+ ? "Cargando grupo…"
93
+ : "El grupo no está disponible.",
94
+ })}
95
+ </p>
96
+ )}
97
+ {usersError && (
98
+ <div role="alert">
99
+ {formatMessage({
100
+ id: "group.members.error",
101
+ defaultMessage: "No se han podido cargar los usuarios.",
102
+ })}
103
+ <button type="button" onClick={() => retryUsers().catch(() => {})}>
104
+ {formatMessage({
105
+ id: "group.members.retry",
106
+ defaultMessage: "Reintentar",
107
+ })}
108
+ </button>
109
+ </div>
110
+ )}
111
+ {error && (
112
+ <div role="alert">
113
+ {formatMessage({ id: "group.unavailable" })}
114
+ <button type="button" onClick={() => retry().catch(() => {})}>
115
+ {formatMessage({
116
+ id: "group.members.retry",
117
+ defaultMessage: "Reintentar",
118
+ })}
119
+ </button>
120
+ </div>
121
+ )}
122
+ </TrueModal>
123
+ {deleting && (
124
+ <GroupDeleteModal
125
+ group={deleting}
126
+ onClose={() => setDeleting(false)}
127
+ onDeleted={onClose}
128
+ />
129
+ )}
130
+ </>
131
+ );
132
+ };
133
+ GroupFormModal.propTypes = {
134
+ editing: PropTypes.bool,
135
+ onClose: PropTypes.func.isRequired,
136
+ };
137
+ export default GroupFormModal;
@@ -0,0 +1,114 @@
1
+ import { useState } from "react";
2
+ import PropTypes from "prop-types";
3
+ import { Link, useParams } from "react-router";
4
+ import { useIntl } from "react-intl";
5
+ import { TrueModal } from "@truedat/core/components/TrueModal";
6
+ import { linkTo } from "@truedat/core/routes";
7
+ import { Button, Icon } from "semantic-ui-react";
8
+ import { useGroup, useGroupUsers } from "../hooks/useGroups";
9
+ import GroupUsersTable from "./GroupUsersTable";
10
+
11
+ const GroupInfoModal = ({ onClose }) => {
12
+ const { id } = useParams();
13
+ const { group, members: groupUsers, loading, error, retry } = useGroup(id);
14
+ const { users } = useGroupUsers();
15
+ const [filter, setFilter] = useState("");
16
+ const empty = Array.isArray(groupUsers) && groupUsers.length === 0;
17
+ const { formatMessage } = useIntl();
18
+ const ready = Boolean(group?.id) && String(group.id) === id;
19
+ return (
20
+ <TrueModal
21
+ open
22
+ centered={false}
23
+ dimmer={{ className: "group-info-dimmer" }}
24
+ size="large"
25
+ className="group-modal group-modal--info"
26
+ icon="users"
27
+ title={
28
+ ready
29
+ ? group.name
30
+ : formatMessage({
31
+ id: "group.loading",
32
+ defaultMessage: "Cargando grupo…",
33
+ })
34
+ }
35
+ subtitle={ready ? group.description || "" : ""}
36
+ onClose={onClose}
37
+ actions={[
38
+ {
39
+ key: "close",
40
+ secondary: true,
41
+ content: formatMessage({ id: "actions.close" }),
42
+ onClick: onClose,
43
+ },
44
+ ...(ready && !empty
45
+ ? [
46
+ {
47
+ key: "edit",
48
+ as: Link,
49
+ to: linkTo.GROUP_EDIT(group),
50
+ primary: true,
51
+ content: formatMessage({ id: "actions.edit" }),
52
+ },
53
+ ]
54
+ : []),
55
+ ]}
56
+ >
57
+ {error ? (
58
+ <div role="alert">
59
+ {formatMessage({ id: "group.unavailable" })}
60
+ <Button type="button" onClick={() => retry().catch(() => {})}>
61
+ {formatMessage({
62
+ id: "group.members.retry",
63
+ defaultMessage: "Reintentar",
64
+ })}
65
+ </Button>
66
+ </div>
67
+ ) : !ready ? (
68
+ <p role="status">
69
+ {formatMessage({
70
+ id: loading ? "group.loading" : "group.unavailable",
71
+ defaultMessage: loading
72
+ ? "Cargando grupo…"
73
+ : "El grupo no está disponible.",
74
+ })}
75
+ </p>
76
+ ) : empty ? (
77
+ <div className="group-members-empty">
78
+ <Icon name="users" aria-hidden="true" />
79
+ <h3>
80
+ {formatMessage({
81
+ id: "group.members.empty.title",
82
+ defaultMessage: "Este grupo no tiene miembros",
83
+ })}
84
+ </h3>
85
+ <p>
86
+ {formatMessage({
87
+ id: "group.members.empty.description",
88
+ defaultMessage: "Edita el grupo para añadir miembros.",
89
+ })}
90
+ </p>
91
+ <Button
92
+ as={Link}
93
+ to={linkTo.GROUP_EDIT(group)}
94
+ primary
95
+ content={formatMessage({ id: "actions.edit" })}
96
+ />
97
+ </div>
98
+ ) : (
99
+ <div className="group-members">
100
+ <GroupUsersTable
101
+ group={group}
102
+ groupUsers={groupUsers}
103
+ users={users}
104
+ readOnly
105
+ filter={filter}
106
+ onFilterChange={setFilter}
107
+ />
108
+ </div>
109
+ )}
110
+ </TrueModal>
111
+ );
112
+ };
113
+ GroupInfoModal.propTypes = { onClose: PropTypes.func.isRequired };
114
+ export default GroupInfoModal;