@truedat/auth 8.5.9 → 8.6.1

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 (27) hide show
  1. package/package.json +5 -5
  2. package/src/groups/components/Group.js +4 -1
  3. package/src/groups/components/GroupCard.js +3 -3
  4. package/src/groups/components/GroupForm.js +16 -0
  5. package/src/groups/components/__tests__/Group.spec.js +68 -6
  6. package/src/groups/components/__tests__/GroupCard.spec.js +15 -1
  7. package/src/groups/components/__tests__/GroupForm.spec.js +80 -0
  8. package/src/groups/reducers/__tests__/groupRedirect.spec.js +4 -8
  9. package/src/groups/reducers/__tests__/groupUsers.spec.js +23 -5
  10. package/src/groups/reducers/group.js +1 -1
  11. package/src/groups/reducers/groupRedirect.js +2 -8
  12. package/src/groups/reducers/groupUsers.js +2 -1
  13. package/src/groups/sagas/__tests__/deleteGroup.spec.js +93 -0
  14. package/src/groups/sagas/deleteGroup.js +2 -1
  15. package/src/messages/en.js +2 -0
  16. package/src/messages/es.js +2 -0
  17. package/src/users/components/GroupUserCrumbs.js +2 -2
  18. package/src/users/components/UserForm.js +26 -14
  19. package/src/users/components/__tests__/GroupUserCrumbs.spec.js +73 -0
  20. package/src/users/components/__tests__/UserForm.spec.js +82 -0
  21. package/src/users/components/__tests__/UserGroupAcls.spec.js +21 -0
  22. package/src/users/components/__tests__/__snapshots__/EditUser.spec.js.snap +2 -8
  23. package/src/users/components/__tests__/__snapshots__/InitialUser.spec.js.snap +2 -8
  24. package/src/users/components/__tests__/__snapshots__/NewUser.spec.js.snap +2 -8
  25. package/src/users/components/__tests__/__snapshots__/UserForm.spec.js.snap +2 -8
  26. package/src/users/selectors/__tests__/getUserGroupAcls.spec.js +26 -23
  27. package/src/users/selectors/getUserGroupAcls.js +4 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/auth",
3
- "version": "8.5.9",
3
+ "version": "8.6.1",
4
4
  "description": "Truedat Web Auth",
5
5
  "sideEffects": false,
6
6
  "jsnext:main": "src/index.js",
@@ -51,15 +51,15 @@
51
51
  "@testing-library/jest-dom": "^6.6.3",
52
52
  "@testing-library/react": "^16.3.0",
53
53
  "@testing-library/user-event": "^14.6.1",
54
- "@truedat/test": "8.5.9",
54
+ "@truedat/test": "8.6.1",
55
55
  "identity-obj-proxy": "^3.0.0",
56
56
  "jest": "^29.7.0",
57
57
  "redux-saga-test-plan": "^4.0.6"
58
58
  },
59
59
  "dependencies": {
60
60
  "@apollo/client": "^3.13.8",
61
- "@truedat/core": "8.5.9",
62
- "auth0-js": "^9.28.0",
61
+ "@truedat/core": "8.6.1",
62
+ "auth0-js": "^10.0.0",
63
63
  "axios": "^1.15.0",
64
64
  "graphql": "^16.11.0",
65
65
  "is-hotkey": "^0.2.0",
@@ -93,5 +93,5 @@
93
93
  "resolutions": {
94
94
  "superagent@npm:^7.1.5": "10.2.3"
95
95
  },
96
- "gitHead": "45e6137b5aa9452db78901e8b0eaa16cc7953b7c"
96
+ "gitHead": "932eb0895ec71961f52a09309b99ae017977a899"
97
97
  }
@@ -62,7 +62,10 @@ export const Group = ({ group, groupLoading, deleteGroup }) => {
62
62
  })}
63
63
  />
64
64
  <Header.Content>
65
- {group.name}{" "}
65
+ {group.alias ? group.alias : group.name}{" "}
66
+ {group.alias ? (
67
+ <Header.Subheader>{group.name}</Header.Subheader>
68
+ ) : null}
66
69
  <Header.Subheader>{group.description}</Header.Subheader>
67
70
  </Header.Content>
68
71
  </Header>
@@ -8,14 +8,14 @@ import { linkTo } from "@truedat/core/routes";
8
8
  import { deleteGroup } from "../routines";
9
9
 
10
10
  export const GroupCard = ({
11
- group: { name, description, id },
11
+ group: { name, description, alias, id },
12
12
  deleteGroup,
13
13
  }) => (
14
14
  <Card key={id}>
15
15
  <Card.Content>
16
16
  <Card.Header as={Link} to={linkTo.GROUP({ id })}>
17
17
  <Icon name="group" />
18
- {name}
18
+ {alias || name}
19
19
  </Card.Header>
20
20
  <Card.Description>{description}</Card.Description>
21
21
  </Card.Content>
@@ -43,7 +43,7 @@ export const GroupCard = ({
43
43
  content={
44
44
  <FormattedMessage
45
45
  id="group.actions.delete.confirmation.content"
46
- values={{ name: <i>{name}</i> }}
46
+ values={{ name: <i>{alias || name}</i> }}
47
47
  />
48
48
  }
49
49
  onConfirm={() => deleteGroup({ id })}
@@ -80,6 +80,22 @@ export const GroupForm = ({
80
80
  />
81
81
  )}
82
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 || ""}
96
+ />
97
+ )}
98
+ />
83
99
  <Controller
84
100
  control={control}
85
101
  name="user_ids"
@@ -1,15 +1,77 @@
1
+ import { fireEvent, waitFor } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
1
3
  import { render } from "@truedat/test/render";
2
- import Group from "../Group";
4
+ import ConnectedGroup, { Group } from "../Group";
3
5
 
4
6
  const group = { id: 2, name: "grupo1 ", description: "aaa bbb cc" };
5
7
 
6
- const renderOpts = {
7
- state: { group, deleteGroup: jest.fn() },
8
- };
9
-
10
8
  describe("<Group />", () => {
11
9
  it("matches the latest snapshot", () => {
12
- const { container } = render(<Group />, renderOpts);
10
+ const { container } = render(
11
+ <Group group={group} deleteGroup={jest.fn()} groupLoading={false} />
12
+ );
13
13
  expect(container).toMatchSnapshot();
14
14
  });
15
+
16
+ it("renders alias with the group name as a subheader when alias is present", () => {
17
+ const group = {
18
+ id: 2,
19
+ name: "grupo1",
20
+ alias: "Grupo Alias",
21
+ description: "aaa bbb cc",
22
+ };
23
+ const { getByText } = render(
24
+ <Group group={group} deleteGroup={jest.fn()} />
25
+ );
26
+
27
+ expect(getByText("Grupo Alias")).toBeInTheDocument();
28
+ expect(getByText("grupo1")).toBeInTheDocument();
29
+ });
30
+
31
+ it("renders nothing while the group is loading", () => {
32
+ const rendered = render(
33
+ <Group group={group} deleteGroup={jest.fn()} groupLoading={true} />
34
+ );
35
+ expect(rendered.container).toBeEmptyDOMElement();
36
+ });
37
+
38
+ it("renders nothing when the group has no id", () => {
39
+ const rendered = render(
40
+ <Group
41
+ group={{ name: "no id", description: "x" }}
42
+ deleteGroup={jest.fn()}
43
+ groupLoading={false}
44
+ />
45
+ );
46
+ expect(rendered.container).toBeEmptyDOMElement();
47
+ });
48
+
49
+ it("maps group state from the store for the connected component", () => {
50
+ const rendered = render(<ConnectedGroup />, {
51
+ state: {
52
+ group: { id: 2, name: "from store", description: "stored desc" },
53
+ groupLoading: false,
54
+ },
55
+ });
56
+ expect(rendered.getByText(/from store/i)).toBeInTheDocument();
57
+ });
58
+
59
+ it("calls deleteGroup with the group id when delete is confirmed", async () => {
60
+ const user = userEvent.setup({ delay: null });
61
+ const deleteGroup = jest.fn();
62
+ const rendered = render(
63
+ <Group group={group} deleteGroup={deleteGroup} groupLoading={false} />
64
+ );
65
+ const dropdown = rendered.getByRole("listbox");
66
+ fireEvent.click(dropdown);
67
+ await waitFor(() =>
68
+ expect(rendered.getByText(/actions.delete/i)).toBeInTheDocument()
69
+ );
70
+ await user.click(rendered.getByText(/actions.delete/i));
71
+ await waitFor(() =>
72
+ expect(rendered.getByText(/confirmation.yes/i)).toBeInTheDocument()
73
+ );
74
+ await user.click(rendered.getByText(/confirmation.yes/i));
75
+ expect(deleteGroup).toHaveBeenCalledWith({ id: group.id });
76
+ });
15
77
  });
@@ -17,6 +17,20 @@ describe("<GroupCard />", () => {
17
17
  expect(rendered.container).toMatchSnapshot();
18
18
  });
19
19
 
20
+ it("renders alias instead of name when alias is present", async () => {
21
+ const aliasGroup = {
22
+ ...group,
23
+ alias: "Admins",
24
+ };
25
+ const rendered = render(
26
+ <GroupCard group={aliasGroup} deleteGroup={deleteGroup} />,
27
+ );
28
+ await waitForLoad(rendered);
29
+
30
+ expect(rendered.getByText("Admins")).toBeInTheDocument();
31
+ expect(rendered.queryByText("administrators")).not.toBeInTheDocument();
32
+ });
33
+
20
34
  it("calls deleteGroup when delete button is clicked", async () => {
21
35
  const rendered = render(<GroupCard {...props} />);
22
36
  await waitForLoad(rendered);
@@ -26,7 +40,7 @@ describe("<GroupCard />", () => {
26
40
  await user.click(rendered.getByRole("button", { name: /delete/i }));
27
41
 
28
42
  await user.click(
29
- rendered.getByRole("button", { name: /modal-affirmative-action/i })
43
+ rendered.getByRole("button", { name: /modal-affirmative-action/i }),
30
44
  );
31
45
 
32
46
  expect(deleteGroup).toHaveBeenCalledTimes(1);
@@ -0,0 +1,80 @@
1
+ import userEvent from "@testing-library/user-event";
2
+ import { waitFor } from "@testing-library/react";
3
+ import { render, waitForLoad } from "@truedat/test/render";
4
+ import GroupForm from "../GroupForm";
5
+
6
+ describe("<GroupForm />", () => {
7
+ const renderOpts = {
8
+ state: {
9
+ users: [{ id: 101, full_name: "John Doe", user_name: "jdoe" }],
10
+ },
11
+ };
12
+
13
+ it("renders the alias input with the default group alias", async () => {
14
+ const group = {
15
+ id: 1,
16
+ name: "Administrators",
17
+ alias: "admin-group",
18
+ description: "Group description",
19
+ };
20
+
21
+ const rendered = render(
22
+ <GroupForm
23
+ group={group}
24
+ groupUsers={[{ id: 101 }]}
25
+ onSubmit={jest.fn()}
26
+ />,
27
+ renderOpts
28
+ );
29
+
30
+ await waitForLoad(rendered);
31
+ await waitFor(() =>
32
+ expect(
33
+ rendered.getByPlaceholderText(/group.form.alias.placeholder/i)
34
+ ).toHaveValue("admin-group")
35
+ );
36
+ });
37
+
38
+ it("updates name, description, alias and users fields", async () => {
39
+ const onSubmit = jest.fn();
40
+ const rendered = render(
41
+ <GroupForm group={{}} groupUsers={[]} onSubmit={onSubmit} />,
42
+ renderOpts
43
+ );
44
+
45
+ await waitForLoad(rendered);
46
+
47
+ const user = userEvent.setup({ delay: null });
48
+
49
+ await user.type(
50
+ rendered.getByPlaceholderText(/group.form.name.placeholder/i),
51
+ "Team Alpha"
52
+ );
53
+ await user.type(
54
+ rendered.getByPlaceholderText(/group.form.description.placeholder/i),
55
+ "Alpha description"
56
+ );
57
+ await user.type(
58
+ rendered.getByPlaceholderText(/group.form.alias.placeholder/i),
59
+ "alpha-team"
60
+ );
61
+
62
+ await user.click(rendered.getByRole("combobox"));
63
+ await user.click(rendered.getByRole("option", { name: /jdoe/i }));
64
+
65
+ await waitFor(() =>
66
+ expect(
67
+ rendered.getByPlaceholderText(/group.form.name.placeholder/i)
68
+ ).toHaveValue("Team Alpha")
69
+ );
70
+ expect(
71
+ rendered.getByPlaceholderText(/group.form.description.placeholder/i)
72
+ ).toHaveValue("Alpha description");
73
+ expect(
74
+ rendered.getByPlaceholderText(/group.form.alias.placeholder/i)
75
+ ).toHaveValue("alpha-team");
76
+ expect(rendered.getByText(/jdoe/i)).toBeInTheDocument();
77
+ expect(onSubmit).not.toHaveBeenCalled();
78
+ expect(rendered.getByRole("button", { name: /save/i })).not.toBeDisabled();
79
+ });
80
+ });
@@ -1,10 +1,9 @@
1
- import { linkTo } from "@truedat/core/routes";
2
1
  import { clearRedirect } from "@truedat/core/routines";
3
2
  import {
4
3
  createGroup,
5
4
  deleteGroup,
6
5
  deleteGroupUser,
7
- updateGroup
6
+ updateGroup,
8
7
  } from "../../routines";
9
8
  import { groupRedirect } from "..";
10
9
 
@@ -40,12 +39,9 @@ describe("reducers: groupRedirect", () => {
40
39
  );
41
40
  });
42
41
 
43
- it("should handle the deleteGroupUser.SUCCESS action", () => {
44
- const meta = { group_id: "1" };
45
- const url = linkTo.GROUP({ id: "1" });
46
-
47
- expect(groupRedirect("foo", { type: deleteGroupUser.SUCCESS, meta })).toBe(
48
- url
42
+ it("should ignore deleteGroupUser.SUCCESS action", () => {
43
+ expect(groupRedirect("foo", { type: deleteGroupUser.SUCCESS })).toEqual(
44
+ "foo"
49
45
  );
50
46
  });
51
47
 
@@ -1,4 +1,4 @@
1
- import { clearGroup, fetchGroup } from "../../routines";
1
+ import { clearGroup, deleteGroupUser, fetchGroup } from "../../routines";
2
2
  import { groupUsers, initialState } from "../groupUsers";
3
3
 
4
4
  const fooState = { foo: "bar" };
@@ -14,20 +14,20 @@ describe("reducers: groupUsers", () => {
14
14
  id: 1,
15
15
  user_name: "user_name1",
16
16
  full_name: "user name 1",
17
- email: "email1@test.com"
17
+ email: "email1@test.com",
18
18
  },
19
19
  {
20
20
  id: 2,
21
21
  user_name: "user_name2",
22
22
  full_name: "user name 2",
23
- email: "email2@test.com"
24
- }
23
+ email: "email2@test.com",
24
+ },
25
25
  ];
26
26
 
27
27
  expect(
28
28
  groupUsers(fooState, {
29
29
  type: fetchGroup.SUCCESS,
30
- payload: { data: { users } }
30
+ payload: { data: { users } },
31
31
  })
32
32
  ).toBe(users);
33
33
  });
@@ -36,6 +36,24 @@ describe("reducers: groupUsers", () => {
36
36
  expect(groupUsers(fooState, { type: clearGroup.TRIGGER })).toEqual([]);
37
37
  });
38
38
 
39
+ it("should handle the deleteGroupUser action", () => {
40
+ const users = [
41
+ {
42
+ id: 2,
43
+ user_name: "user_name2",
44
+ full_name: "user name 2",
45
+ email: "email2@test.com",
46
+ },
47
+ ];
48
+
49
+ expect(
50
+ groupUsers(fooState, {
51
+ type: deleteGroupUser.SUCCESS,
52
+ payload: { data: { users } },
53
+ })
54
+ ).toBe(users);
55
+ });
56
+
39
57
  it("should ignore unknown actions", () => {
40
58
  expect(groupUsers(fooState, { type: "FOO" })).toBe(fooState);
41
59
  });
@@ -3,7 +3,7 @@ import { clearGroup, fetchGroup, updateGroup, createGroup } from "../routines";
3
3
 
4
4
  const initialState = {};
5
5
 
6
- const pickFields = _.pick(["id", "name", "description"]);
6
+ const pickFields = _.pick(["id", "name", "description", "alias"]);
7
7
 
8
8
  const group = (state = initialState, { type, payload }) => {
9
9
  switch (type) {
@@ -1,12 +1,10 @@
1
- import _ from "lodash/fp";
2
1
  import { clearRedirect } from "@truedat/core/routines";
3
- import { linkTo, GROUPS } from "@truedat/core/routes";
2
+ import { GROUPS } from "@truedat/core/routes";
4
3
  import { createGroup, deleteGroup, updateGroup } from "../routines";
5
- import { deleteGroupUser } from "../../groups/routines";
6
4
 
7
5
  const initialState = "";
8
6
 
9
- export const groupRedirect = (state = initialState, { type, meta }) => {
7
+ export const groupRedirect = (state = initialState, { type }) => {
10
8
  switch (type) {
11
9
  case clearRedirect.TRIGGER:
12
10
  return initialState;
@@ -16,10 +14,6 @@ export const groupRedirect = (state = initialState, { type, meta }) => {
16
14
  return GROUPS;
17
15
  case deleteGroup.SUCCESS:
18
16
  return GROUPS;
19
- case deleteGroupUser.SUCCESS: {
20
- const id = _.prop("group_id")(meta);
21
- return linkTo.GROUP({ id });
22
- }
23
17
  default:
24
18
  return state;
25
19
  }
@@ -1,11 +1,12 @@
1
1
  import _ from "lodash/fp";
2
- import { clearGroup, fetchGroup } from "../routines";
2
+ import { clearGroup, deleteGroupUser, fetchGroup } from "../routines";
3
3
 
4
4
  export const initialState = [];
5
5
 
6
6
  export const groupUsers = (state = initialState, { type, payload }) => {
7
7
  switch (type) {
8
8
  case fetchGroup.SUCCESS:
9
+ case deleteGroupUser.SUCCESS:
9
10
  return _.pathOr([], "data.users")(payload);
10
11
  case clearGroup.TRIGGER:
11
12
  return initialState;
@@ -0,0 +1,93 @@
1
+ import { compile } from "path-to-regexp";
2
+ import { testSaga } from "redux-saga-test-plan";
3
+ import { apiJsonDelete, JSON_OPTS } from "@truedat/core/services/api";
4
+ import { deleteGroupRequestSaga, deleteGroupSaga } from "../deleteGroup";
5
+ import { deleteGroup, fetchGroups } from "../../routines";
6
+ import { API_GROUP } from "../../api";
7
+
8
+ describe("sagas: deleteGroupRequestSaga", () => {
9
+ it("should invoke deleteGroupSaga on deleteGroup.TRIGGER", () => {
10
+ expect(() => {
11
+ testSaga(deleteGroupRequestSaga)
12
+ .next()
13
+ .takeLatest(deleteGroup.TRIGGER, deleteGroupSaga)
14
+ .finish()
15
+ .isDone();
16
+ }).not.toThrow();
17
+ });
18
+
19
+ it("should throw exception if an unhandled action is received", () => {
20
+ expect(() => {
21
+ testSaga(deleteGroupRequestSaga)
22
+ .next()
23
+ .takeLatest("FOO", deleteGroupRequestSaga);
24
+ }).toThrow();
25
+ });
26
+ });
27
+
28
+ describe("sagas: deleteGroupSaga", () => {
29
+ const payload = { id: "1", name: "group-1" };
30
+ const url = compile(API_GROUP)({ id: `${payload.id}` });
31
+
32
+ it("should put a success action and fetchGroups when a response is returned", () => {
33
+ expect(() => {
34
+ testSaga(deleteGroupSaga, { payload })
35
+ .next()
36
+ .put(deleteGroup.request())
37
+ .next()
38
+ .call(apiJsonDelete, url, JSON_OPTS)
39
+ .next({ data: payload })
40
+ .put(deleteGroup.success(payload))
41
+ .next()
42
+ .put(fetchGroups())
43
+ .next()
44
+ .put(deleteGroup.fulfill())
45
+ .next()
46
+ .isDone();
47
+ }).not.toThrow();
48
+ });
49
+
50
+ it("should put a failure action when the call returns an error response", () => {
51
+ const error = {
52
+ response: { status: 500, data: { message: "Request failed" } },
53
+ };
54
+
55
+ expect(() => {
56
+ testSaga(deleteGroupSaga, { payload })
57
+ .next()
58
+ .put(deleteGroup.request())
59
+ .next()
60
+ .call(apiJsonDelete, url, JSON_OPTS)
61
+ .throw(error)
62
+ .put(
63
+ deleteGroup.failure({
64
+ status: 500,
65
+ data: { message: "Request failed" },
66
+ })
67
+ )
68
+ .next()
69
+ .put(deleteGroup.fulfill())
70
+ .next()
71
+ .isDone();
72
+ }).not.toThrow();
73
+ });
74
+
75
+ it("should put a failure action when the call returns an error message", () => {
76
+ const message = "Request failed";
77
+ const error = { message };
78
+
79
+ expect(() => {
80
+ testSaga(deleteGroupSaga, { payload })
81
+ .next()
82
+ .put(deleteGroup.request())
83
+ .next()
84
+ .call(apiJsonDelete, url, JSON_OPTS)
85
+ .throw(error)
86
+ .put(deleteGroup.failure(message))
87
+ .next()
88
+ .put(deleteGroup.fulfill())
89
+ .next()
90
+ .isDone();
91
+ }).not.toThrow();
92
+ });
93
+ });
@@ -1,7 +1,7 @@
1
1
  import { compile } from "path-to-regexp";
2
2
  import { call, put, takeLatest } from "redux-saga/effects";
3
3
  import { apiJsonDelete, JSON_OPTS } from "@truedat/core/services/api";
4
- import { deleteGroup } from "../routines";
4
+ import { deleteGroup, fetchGroups } from "../routines";
5
5
  import { API_GROUP } from "../api";
6
6
 
7
7
  const toApiPath = compile(API_GROUP);
@@ -13,6 +13,7 @@ export function* deleteGroupSaga({ payload }) {
13
13
  yield put(deleteGroup.request());
14
14
  const { data } = yield call(apiJsonDelete, url, JSON_OPTS);
15
15
  yield put(deleteGroup.success(data));
16
+ yield put(fetchGroups());
16
17
  } catch (error) {
17
18
  if (error.response) {
18
19
  const { status, data } = error.response;
@@ -13,10 +13,12 @@ export default {
13
13
  "group.actions.delete.confirmation.header": "Delete Group",
14
14
  "group.form.description.placeholder": "Description",
15
15
  "group.form.name.placeholder": "Group Name",
16
+ "group.form.alias.placeholder": "Group Alias",
16
17
  "group.form.users.placeholder": "Select user",
17
18
  "group.props.description": "Description",
18
19
  "group.props.members": "{members} users",
19
20
  "group.props.name": "Name",
21
+ "group.props.alias": "Alias",
20
22
  "group.props.users": "Users",
21
23
  "groupUsers.actions.delete.confirmation.content": "Selected user will be deleted from group, ¿are you sure?",
22
24
  "groupUsers.actions.delete.confirmation.header": "Remove user from group",
@@ -13,10 +13,12 @@ export default {
13
13
  "group.actions.delete.confirmation.header": "Eliminar grupo",
14
14
  "group.form.description.placeholder": "Description",
15
15
  "group.form.name.placeholder": "Nombre del grupo",
16
+ "group.form.alias.placeholder": "Alias del grupo",
16
17
  "group.form.users.placeholder": "Seleccionar usuario",
17
18
  "group.props.description": "Description",
18
19
  "group.props.members": "{members} usuarios",
19
20
  "group.props.name": "Nombre",
21
+ "group.props.alias": "Alias",
20
22
  "group.props.users": "Usuarios",
21
23
  "groupUsers.actions.delete.confirmation.content": "Se va a eliminar al usuario seleccionado del grupo, ¿está seguro?",
22
24
  "groupUsers.actions.delete.confirmation.header": "Eliminar usuario de grupo",
@@ -15,7 +15,7 @@ export const GroupUserCrumbs = ({ group, groupLoading, action }) => {
15
15
  {action === "show" && (
16
16
  <>
17
17
  <Breadcrumb.Divider icon="right angle" />
18
- {group.name}
18
+ {group.alias || group.name}
19
19
  </>
20
20
  )}
21
21
  {action === "add" && (
@@ -26,7 +26,7 @@ export const GroupUserCrumbs = ({ group, groupLoading, action }) => {
26
26
  to={linkTo.GROUP({ id: group.id })}
27
27
  active={action === "show"}
28
28
  >
29
- {group.name}
29
+ {group.alias || group.name}
30
30
  </Breadcrumb.Section>
31
31
  <Breadcrumb.Divider icon="right angle" />
32
32
  <FormattedMessage id="groups.crumbs.groupUser.add" />
@@ -6,17 +6,29 @@ import { Button, Form } from "semantic-ui-react";
6
6
  import { connect } from "react-redux";
7
7
  import { HistoryBackButton } from "@truedat/core/components";
8
8
 
9
- const toOption = (g, i) => ({ key: i, text: g, value: g });
9
+ const groupOptions = (groups = [], values) => {
10
+ const groupOpts = _.map.convert({ cap: false })((g, i) => {
11
+ const displayText = typeof g === "string" ? g : g.alias || g.name || "";
12
+ const value = typeof g === "string" ? g : g.name || "";
13
+ return { key: `group_${i}`, text: displayText, value: value };
14
+ })(groups);
10
15
 
11
- const groupOptions = (groups, values) =>
12
- _.flow(
13
- _.concat(""),
14
- _.concat(values),
16
+ const valueOpts = _.flow(
15
17
  _.filter(_.isString),
18
+ _.reject((value) =>
19
+ _.some((g) => (typeof g === "string" ? g : g.name) === value, groups),
20
+ ),
16
21
  _.uniq,
17
22
  _.sortBy(_.lowerCase),
18
- _.map.convert({ cap: false })(toOption)
19
- )(groups);
23
+ _.map.convert({ cap: false })((text, i) => ({
24
+ key: `value_${i}`,
25
+ text,
26
+ value: text,
27
+ })),
28
+ )(values || []);
29
+
30
+ return _.concat(groupOpts, valueOpts);
31
+ };
20
32
 
21
33
  const userTypeOptions = (formatMessage) => [
22
34
  { text: formatMessage({ id: "user.type.user" }), value: "user" },
@@ -41,13 +53,13 @@ export const PasswordFormFields = ({ control, errors }) => {
41
53
  rules={{
42
54
  required: formatMessage(
43
55
  { id: "form.validation.required" },
44
- { prop: formatMessage({ id: "user.form.password" }) }
56
+ { prop: formatMessage({ id: "user.form.password" }) },
45
57
  ),
46
58
  minLength: {
47
59
  value: 6,
48
60
  message: formatMessage(
49
61
  { id: "form.validation.minLength" },
50
- { prop: formatMessage({ id: "user.form.password" }), value: 6 }
62
+ { prop: formatMessage({ id: "user.form.password" }), value: 6 },
51
63
  ),
52
64
  },
53
65
  }}
@@ -132,7 +144,7 @@ export const UserForm = ({ isSubmitting, onSubmit, user, groups }) => {
132
144
  rules={{
133
145
  required: formatMessage(
134
146
  { id: "form.validation.required" },
135
- { prop: formatMessage({ id: "user.form.user_name" }) }
147
+ { prop: formatMessage({ id: "user.form.user_name" }) },
136
148
  ),
137
149
  }}
138
150
  render={({ field: { onBlur, onChange, value } }) => (
@@ -216,7 +228,7 @@ export const UserForm = ({ isSubmitting, onSubmit, user, groups }) => {
216
228
  error
217
229
  ? formatMessage(
218
230
  { id: "invalid", defaultMessage: "{prop} is required" },
219
- { prop: formatMessage({ id: "user.form.full_name" }) }
231
+ { prop: formatMessage({ id: "user.form.full_name" }) },
220
232
  )
221
233
  : false
222
234
  }
@@ -265,7 +277,7 @@ export const UserForm = ({ isSubmitting, onSubmit, user, groups }) => {
265
277
  onBlur={onBlur}
266
278
  onChange={(_e, { value }) => onChange(value)}
267
279
  placeholder={formatMessage({ id: "user.form.placeholder.groups" })}
268
- value={value || ""}
280
+ value={value || []}
269
281
  />
270
282
  )}
271
283
  />
@@ -294,12 +306,12 @@ UserForm.propTypes = {
294
306
  isSubmitting: PropTypes.bool,
295
307
  onSubmit: PropTypes.func,
296
308
  user: PropTypes.object,
297
- groups: PropTypes.arrayOf(PropTypes.string),
309
+ groups: PropTypes.array,
298
310
  };
299
311
 
300
312
  const mapStateToProps = ({ userUpdating, userCreating, groups }) => ({
301
313
  isSubmitting: userUpdating || userCreating,
302
- groups: _.map("name")(groups),
314
+ groups: groups || [],
303
315
  });
304
316
 
305
317
  export default connect(mapStateToProps)(UserForm);
@@ -0,0 +1,73 @@
1
+ import { render } from "@truedat/test/render";
2
+ import { GroupUserCrumbs } from "../GroupUserCrumbs";
3
+
4
+ describe("<GroupUserCrumbs />", () => {
5
+ const group = {
6
+ id: "1",
7
+ name: "administrators",
8
+ alias: "Admins",
9
+ };
10
+
11
+ it("renders alias when action is show", () => {
12
+ const rendered = render(
13
+ <GroupUserCrumbs group={group} action="show" groupLoading={false} />
14
+ );
15
+
16
+ expect(rendered.getByText("Admins")).toBeInTheDocument();
17
+ expect(rendered.queryByText("administrators")).not.toBeInTheDocument();
18
+ });
19
+
20
+ it("renders alias link when action is add", () => {
21
+ const rendered = render(
22
+ <GroupUserCrumbs group={group} action="add" groupLoading={false} />
23
+ );
24
+
25
+ expect(rendered.getByRole("link", { name: /Admins/i })).toBeInTheDocument();
26
+ });
27
+
28
+ it("renders nothing while group is loading", () => {
29
+ const rendered = render(
30
+ <GroupUserCrumbs group={group} action="show" groupLoading />
31
+ );
32
+
33
+ expect(rendered.container).toBeEmptyDOMElement();
34
+ });
35
+
36
+ it("renders nothing when group has no name", () => {
37
+ const rendered = render(
38
+ <GroupUserCrumbs
39
+ group={{ id: "1", name: "", alias: "Admins" }}
40
+ action="show"
41
+ groupLoading={false}
42
+ />
43
+ );
44
+
45
+ expect(rendered.container).toBeEmptyDOMElement();
46
+ });
47
+
48
+ it("renders group name when alias is missing", () => {
49
+ const rendered = render(
50
+ <GroupUserCrumbs
51
+ group={{ id: "1", name: "administrators" }}
52
+ action="add"
53
+ groupLoading={false}
54
+ />
55
+ );
56
+
57
+ expect(
58
+ rendered.getByRole("link", { name: /administrators/i })
59
+ ).toBeInTheDocument();
60
+ });
61
+
62
+ it("renders group name on show when alias is missing", () => {
63
+ const rendered = render(
64
+ <GroupUserCrumbs
65
+ group={{ id: "1", name: "administrators" }}
66
+ action="show"
67
+ groupLoading={false}
68
+ />
69
+ );
70
+
71
+ expect(rendered.getByText(/administrators/i)).toBeInTheDocument();
72
+ });
73
+ });
@@ -98,4 +98,86 @@ describe("<UserForm />", () => {
98
98
  })
99
99
  );
100
100
  });
101
+
102
+ it("shows group alias values in the groups dropdown", async () => {
103
+ const groups = [
104
+ { name: "admins", alias: "Administrators" },
105
+ { name: "users", alias: "Users" },
106
+ ];
107
+ const rendered = render(<UserForm groups={groups} />, {
108
+ ...renderOpts,
109
+ state: { ...renderOpts.state, groups },
110
+ });
111
+
112
+ await waitForLoad(rendered);
113
+
114
+ const user = userEvent.setup({ delay: null });
115
+ await user.click(rendered.getByRole("combobox"));
116
+
117
+ await waitFor(() =>
118
+ expect(rendered.getByText("Administrators")).toBeInTheDocument()
119
+ );
120
+ expect(rendered.getByText("Users")).toBeInTheDocument();
121
+ });
122
+
123
+ it("submits external id and custom groups values", async () => {
124
+ const onSubmit = jest.fn();
125
+ const groups = [
126
+ { name: "admins", alias: "Administrators" },
127
+ { name: "users", alias: "Users" },
128
+ ];
129
+ const rendered = render(
130
+ <UserForm
131
+ onSubmit={(props) => onSubmit(props)}
132
+ groups={groups}
133
+ user={{
134
+ id: 123,
135
+ user_name: "fred",
136
+ full_name: "fredrik foobar",
137
+ groups: ["zzz", "admins", "ZZZ"],
138
+ }}
139
+ />,
140
+ {
141
+ ...renderOpts,
142
+ state: {
143
+ ...renderOpts.state,
144
+ groups,
145
+ },
146
+ }
147
+ );
148
+ await waitForLoad(rendered);
149
+
150
+ const user = userEvent.setup({ delay: null });
151
+ const groupsDropdown = rendered.container.querySelector(
152
+ '.ui.dropdown[name="groups"]'
153
+ );
154
+ const externalIdInput = rendered.getByRole("textbox", {
155
+ name: /external_id/i,
156
+ });
157
+ const selectedGroupDeleteIcon = rendered.container.querySelector(
158
+ '.ui.dropdown[name="groups"] .ui.label[value="zzz"] .delete.icon'
159
+ );
160
+
161
+ await user.click(groupsDropdown);
162
+ await user.click(selectedGroupDeleteIcon);
163
+ await user.clear(externalIdInput);
164
+ await user.type(externalIdInput, "ext-123");
165
+
166
+ await waitFor(() =>
167
+ expect(rendered.getByRole("button", { name: /save/i })).not.toBeDisabled()
168
+ );
169
+
170
+ await user.click(rendered.getByRole("button", { name: /save/i }));
171
+
172
+ await waitFor(() =>
173
+ expect(onSubmit).toHaveBeenCalledWith({
174
+ email: "",
175
+ external_id: "ext-123",
176
+ full_name: "fredrik foobar",
177
+ groups: ["admins", "ZZZ"],
178
+ role: "user",
179
+ user_name: "fred",
180
+ })
181
+ );
182
+ });
101
183
  });
@@ -8,6 +8,8 @@ const renderOpts = {
8
8
  "user.acl.role": "role",
9
9
  "user.acl.domain": "domain",
10
10
  "user.acl": "acl",
11
+ "user.acl.group": "group",
12
+ "user.group.acl": "group acl",
11
13
  },
12
14
  },
13
15
  state: {
@@ -33,4 +35,23 @@ describe("<UserGroupAcls />", () => {
33
35
  const { container } = render(<UserGroupAcls />, renderOpts);
34
36
  await waitFor(() => expect(container).toMatchSnapshot());
35
37
  });
38
+
39
+ it("renders group alias when provided by the selector", async () => {
40
+ const rendered = render(
41
+ <UserGroupAcls
42
+ acls={[
43
+ {
44
+ resource: "resource1",
45
+ role: "role1",
46
+ group: "Group One",
47
+ },
48
+ ]}
49
+ />,
50
+ renderOpts
51
+ );
52
+
53
+ await waitFor(() =>
54
+ expect(rendered.queryByText(/group one/i)).toBeInTheDocument()
55
+ );
56
+ });
36
57
  });
@@ -248,15 +248,9 @@ exports[`<EditUser /> matches the latest snapshot 1`] = `
248
248
  role="listbox"
249
249
  >
250
250
  <div
251
- aria-checked="false"
252
- aria-selected="true"
253
- class="selected item"
254
- role="option"
255
- style="pointer-events: all;"
251
+ class="message"
256
252
  >
257
- <span
258
- class="text"
259
- />
253
+ No results found.
260
254
  </div>
261
255
  </div>
262
256
  </div>
@@ -228,15 +228,9 @@ exports[`<InitialUser /> matches the latest snapshot 1`] = `
228
228
  role="listbox"
229
229
  >
230
230
  <div
231
- aria-checked="false"
232
- aria-selected="true"
233
- class="selected item"
234
- role="option"
235
- style="pointer-events: all;"
231
+ class="message"
236
232
  >
237
- <span
238
- class="text"
239
- />
233
+ No results found.
240
234
  </div>
241
235
  </div>
242
236
  </div>
@@ -248,15 +248,9 @@ exports[`<NewUser /> matches the latest snapshot 1`] = `
248
248
  role="listbox"
249
249
  >
250
250
  <div
251
- aria-checked="false"
252
- aria-selected="true"
253
- class="selected item"
254
- role="option"
255
- style="pointer-events: all;"
251
+ class="message"
256
252
  >
257
- <span
258
- class="text"
259
- />
253
+ No results found.
260
254
  </div>
261
255
  </div>
262
256
  </div>
@@ -212,15 +212,9 @@ exports[`<UserForm /> matches the latest snapshot 1`] = `
212
212
  role="listbox"
213
213
  >
214
214
  <div
215
- aria-checked="false"
216
- aria-selected="true"
217
- class="selected item"
218
- role="option"
219
- style="pointer-events: all;"
215
+ class="message"
220
216
  >
221
- <span
222
- class="text"
223
- />
217
+ No results found.
224
218
  </div>
225
219
  </div>
226
220
  </div>
@@ -1,40 +1,43 @@
1
- import { getUserAcls } from "..";
1
+ import { getUserGroupAcls } from "..";
2
2
 
3
- describe("selectors: getUserAcls", () => {
4
- const acl1 = {
3
+ describe("selectors: getUserGroupAcls", () => {
4
+ const aclWithAlias = {
5
5
  resource: { id: 1, name: "resource1", type: "domain" },
6
- role: { id: 2, name: "role1" }
6
+ role: { id: 2, name: "role1" },
7
+ group: { id: 6, name: "group5", alias: "Group Five" },
7
8
  };
8
9
 
9
- const acl2 = {
10
+ const aclWithoutAlias = {
10
11
  resource: { id: 2, name: "resource2", type: "domain" },
11
- role: { id: 3, name: "role2" }
12
+ role: { id: 3, name: "role2" },
13
+ group: { id: 7, name: "group6" },
12
14
  };
13
15
 
14
- const acl3 = {
16
+ const aclWithoutGroup = {
15
17
  resource: { id: 4, name: "resource3", type: "domain" },
16
18
  role: { id: 5, name: "role5" },
17
- group: { id: 6, name: "group5" }
18
19
  };
19
20
 
20
- const acls = [acl1, acl2, acl3];
21
+ const user = { acls: [aclWithAlias, aclWithoutAlias, aclWithoutGroup] };
21
22
 
22
- const user = { acls: acls };
23
+ it("returns only group ACLs and prefers group.alias over group.name", () => {
24
+ const res = getUserGroupAcls({ user });
23
25
 
24
- it("should return user acls", () => {
25
- const res = getUserAcls({ user });
26
- expect(res).toHaveLength(2);
27
- expect(res).toEqual(
28
- expect.arrayContaining([
29
- { resource: acl1.resource.name, role: acl1.role.name },
30
- { resource: acl2.resource.name, role: acl2.role.name }
31
- ])
32
- );
26
+ expect(res).toEqual([
27
+ {
28
+ resource: aclWithAlias.resource.name,
29
+ role: aclWithAlias.role.name,
30
+ group: aclWithAlias.group.alias,
31
+ },
32
+ {
33
+ resource: aclWithoutAlias.resource.name,
34
+ role: aclWithoutAlias.role.name,
35
+ group: aclWithoutAlias.group.name,
36
+ },
37
+ ]);
33
38
  });
34
39
 
35
- it("should return empty array when we have no acls", () => {
36
- const res = getUserAcls({ user: { acls: [] } });
37
- expect(res).toHaveLength(0);
38
- expect(res).toEqual(expect.arrayContaining([]));
40
+ it("returns an empty array when the user has no ACLs", () => {
41
+ expect(getUserGroupAcls({ user: { acls: [] } })).toEqual([]);
39
42
  });
40
43
  });
@@ -2,15 +2,15 @@ import _ from "lodash/fp";
2
2
  import { createSelector } from "reselect";
3
3
  import { getUser } from "./getUser";
4
4
 
5
- const isGroupAcl = m => !_.flow(_.prop("group"), _.isEmpty)(m);
5
+ const isGroupAcl = (m) => !_.flow(_.prop("group"), _.isEmpty)(m);
6
6
 
7
- const buildGroupAcl = m => ({
7
+ const buildGroupAcl = (m) => ({
8
8
  resource: m.resource.name,
9
9
  role: m.role.name,
10
- group: m.group.name
10
+ group: m.group.alias || m.group.name,
11
11
  });
12
12
 
13
- export const getUserGroupAcls = createSelector([getUser], user =>
13
+ export const getUserGroupAcls = createSelector([getUser], (user) =>
14
14
  _.flow(
15
15
  _.prop("acls"),
16
16
  _.filter(isGroupAcl),