@truedat/dd 4.30.2 → 4.30.6

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 (29) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/package.json +6 -6
  3. package/src/api.js +2 -0
  4. package/src/components/GrantRequest.js +12 -1
  5. package/src/components/GrantRequestApprovalForm.js +127 -0
  6. package/src/components/GrantRequestApprovalPopup.js +43 -0
  7. package/src/components/GrantRequestApprovals.js +41 -0
  8. package/src/components/GrantRoutes.js +21 -5
  9. package/src/components/MyGrantRequests.js +2 -2
  10. package/src/components/StructureGrantCart.js +1 -4
  11. package/src/components/__tests__/GrantRequestApprovalForm.spec.js +87 -0
  12. package/src/components/__tests__/GrantRequestApprovalPopup.spec.js +16 -0
  13. package/src/components/__tests__/__snapshots__/GrantRequest.spec.js.snap +12 -1
  14. package/src/components/__tests__/__snapshots__/GrantRequestApprovalForm.spec.js.snap +101 -0
  15. package/src/components/__tests__/__snapshots__/GrantRequestApprovalPopup.spec.js.snap +37 -0
  16. package/src/components/__tests__/__snapshots__/GrantRoutes.spec.js.snap +4 -0
  17. package/src/messages/en.js +16 -5
  18. package/src/messages/es.js +18 -6
  19. package/src/reducers/__tests__/creatingGrantRequestApproval.spec.js +33 -0
  20. package/src/reducers/creatingGrantRequestApproval.js +16 -0
  21. package/src/reducers/index.js +2 -0
  22. package/src/reducers/structureRedirect.js +4 -0
  23. package/src/routines.js +4 -0
  24. package/src/sagas/__tests__/createGrantRequestApproval.spec.js +80 -0
  25. package/src/sagas/createGrantRequestApproval.js +31 -0
  26. package/src/sagas/index.js +3 -0
  27. package/src/selectors/__tests__/getGrantRequests.spec.js +3 -3
  28. package/src/selectors/getGrantRequests.js +1 -1
  29. package/src/selectors/getGrantRequestsColumns.js +12 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [4.30.6] 2021-10-15
4
+
5
+ ### Fixed
6
+
7
+ - [TD-3874] Allow rule creation specifying domain for shared concepts
8
+
9
+ ## [4.30.5] 2021-10-15
10
+
11
+ ### Added
12
+
13
+ - [TD-4226] i18n for grant checkout button
14
+
15
+ ## [4.30.3] 2021-10-07
16
+
17
+ ### Added
18
+
19
+ - [TD-4044] Support for approving grant requests
20
+
3
21
  ## [4.30.1] 2021-10-06
4
22
 
5
23
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/dd",
3
- "version": "4.30.2",
3
+ "version": "4.30.6",
4
4
  "description": "Truedat Web Data Dictionary",
5
5
  "sideEffects": false,
6
6
  "jsnext:main": "src/index.js",
@@ -34,7 +34,7 @@
34
34
  "@testing-library/jest-dom": "^5.14.1",
35
35
  "@testing-library/react": "^12.0.0",
36
36
  "@testing-library/user-event": "^13.2.1",
37
- "@truedat/test": "4.30.1",
37
+ "@truedat/test": "4.30.6",
38
38
  "babel-jest": "^27.0.6",
39
39
  "babel-plugin-dynamic-import-node": "^2.3.3",
40
40
  "babel-plugin-lodash": "^3.3.4",
@@ -84,9 +84,9 @@
84
84
  ]
85
85
  },
86
86
  "dependencies": {
87
- "@truedat/auth": "4.30.2",
88
- "@truedat/core": "4.30.2",
89
- "@truedat/df": "4.30.2",
87
+ "@truedat/auth": "4.30.6",
88
+ "@truedat/core": "4.30.6",
89
+ "@truedat/df": "4.30.6",
90
90
  "lodash": "^4.17.21",
91
91
  "path-to-regexp": "^1.7.0",
92
92
  "prop-types": "^15.7.2",
@@ -107,5 +107,5 @@
107
107
  "react-dom": ">= 16.8.6 < 17",
108
108
  "semantic-ui-react": ">= 0.88.2 < 2.1"
109
109
  },
110
- "gitHead": "d1b2a5b74458267ecb68a30eb3cc331a6899a67f"
110
+ "gitHead": "f23b3cf877ac9383f2e638ddcd685d173ac6142a"
111
111
  }
package/src/api.js CHANGED
@@ -7,6 +7,7 @@ const API_DATA_STRUCTURE_VERSION = "/api/data_structures/:id/versions/:version";
7
7
  const API_GRANT_FILTERS_SEARCH = "/api/grant_filters/search";
8
8
  const API_PROFILE_EXECUTION = "/api/profile_executions/:id";
9
9
  const API_GRANT_REQUEST = "/api/grant_requests/:id";
10
+ const API_GRANT_REQUEST_APPROVALS = "/api/grant_requests/:id/approvals";
10
11
  const API_GRANT_REQUEST_GROUP = "/api/grant_request_groups";
11
12
  const API_GRANT_REQUESTS = "/api/grant_requests";
12
13
  const API_GRANTS_CSV = "/api/grants/csv";
@@ -49,6 +50,7 @@ export {
49
50
  API_PROFILE_EXECUTION,
50
51
  API_GRANT_FILTERS_SEARCH,
51
52
  API_GRANT_REQUEST,
53
+ API_GRANT_REQUEST_APPROVALS,
52
54
  API_GRANT_REQUEST_GROUP,
53
55
  API_GRANT_REQUESTS,
54
56
  API_GRANTS_SEARCH,
@@ -8,6 +8,7 @@ import { useIntl } from "react-intl";
8
8
  import { Header, Grid, Icon, Segment, List, Label } from "semantic-ui-react";
9
9
  import { getGrantRequestTemplate } from "../selectors";
10
10
  import GrantRequestCrumbs from "./GrantRequestCrumbs";
11
+ import GrantRequestApprovalPopup from "./GrantRequestApprovalPopup";
11
12
 
12
13
  const DynamicFormViewer = React.lazy(() =>
13
14
  import("@truedat/df/components/DynamicFormViewer")
@@ -44,7 +45,17 @@ export const GrantRequest = ({ grantRequest, template }) => {
44
45
  </Header>
45
46
  </Grid.Column>
46
47
  <Grid.Column width={4} textAlign="right">
47
- {/*TODO :: <GrantRequestActions /> */}
48
+ <GrantRequestApprovalPopup
49
+ content={formatMessage({ id: "grantRequest.actions.approve" })}
50
+ primary
51
+ grantRequest={grantRequest}
52
+ />
53
+ <GrantRequestApprovalPopup
54
+ content={formatMessage({ id: "grantRequest.actions.reject" })}
55
+ secondary
56
+ isRejection
57
+ grantRequest={grantRequest}
58
+ />
48
59
  </Grid.Column>
49
60
  </Grid>
50
61
  <List size="big" relaxed="very">
@@ -0,0 +1,127 @@
1
+ import _ from "lodash/fp";
2
+ import React from "react";
3
+ import { useForm, Controller } from "react-hook-form";
4
+ import { Button, Container, Form, Header } from "semantic-ui-react";
5
+ import PropTypes from "prop-types";
6
+ import { useIntl } from "react-intl";
7
+ import { connect } from "react-redux";
8
+ import { createGrantRequestApproval } from "../routines";
9
+
10
+ export const GrantRequestApprovalForm = ({
11
+ grantRequest,
12
+ isRejection,
13
+ saving,
14
+ createGrantRequestApproval,
15
+ }) => {
16
+ const { formatMessage } = useIntl();
17
+
18
+ const { handleSubmit, control, formState } = useForm({
19
+ mode: "all",
20
+ defaultValues: {
21
+ role: null,
22
+ comments: "",
23
+ },
24
+ });
25
+ const { isDirty, isValid } = formState;
26
+
27
+ const dropdownOptions = _.flow(
28
+ _.get("pending_roles"),
29
+ _.map((role) => ({ key: role, value: role, text: role }))
30
+ )(grantRequest);
31
+
32
+ const onSubmit = (formValues) => {
33
+ const { id, domain_id } = grantRequest;
34
+ const approval = {
35
+ ...formValues,
36
+ domain_id,
37
+ is_rejection: isRejection,
38
+ };
39
+ createGrantRequestApproval({
40
+ grantRequestId: id,
41
+ approval,
42
+ });
43
+ };
44
+ return (
45
+ <Container style={{ width: "400px" }}>
46
+ <Header
47
+ as="h2"
48
+ content={formatMessage({
49
+ id: isRejection
50
+ ? "grantRequestApproval.form.reject"
51
+ : "grantRequestApproval.form.approve",
52
+ })}
53
+ />
54
+ <Form onSubmit={handleSubmit(onSubmit)}>
55
+ <Controller
56
+ control={control}
57
+ name="role"
58
+ rules={{ validate: (v) => !_.isEmpty(v) }}
59
+ render={({ onBlur, onChange, value }, { invalid }) => (
60
+ <Form.Dropdown
61
+ placeholder={formatMessage({
62
+ id: "grantRequestApproval.form.role.placeholder",
63
+ })}
64
+ label={formatMessage({
65
+ id: "grantRequestApproval.form.role.label",
66
+ })}
67
+ error={invalid}
68
+ onBlur={onBlur}
69
+ value={value}
70
+ onChange={(_, { value }) => onChange(value)}
71
+ selection
72
+ required
73
+ options={dropdownOptions}
74
+ />
75
+ )}
76
+ />
77
+ <Controller
78
+ control={control}
79
+ name="comments"
80
+ render={({ onBlur, onChange, value }, { invalid }) => (
81
+ <Form.TextArea
82
+ data-testid="comments-id"
83
+ label={formatMessage({
84
+ id: "grantRequestApproval.form.comments.label",
85
+ })}
86
+ placeholder={formatMessage({
87
+ id: "grantRequestApproval.form.comments.placeholder",
88
+ })}
89
+ value={value}
90
+ onBlur={onBlur}
91
+ error={invalid}
92
+ onChange={(_, { value }) => onChange(value)}
93
+ />
94
+ )}
95
+ />
96
+ <div className="actions">
97
+ <Button
98
+ type="submit"
99
+ disabled={!isDirty || !isValid}
100
+ loading={saving}
101
+ primary
102
+ content={formatMessage({
103
+ id: isRejection
104
+ ? "grantRequest.actions.reject"
105
+ : "grantRequest.actions.approve",
106
+ })}
107
+ />
108
+ </div>
109
+ </Form>
110
+ </Container>
111
+ );
112
+ };
113
+
114
+ GrantRequestApprovalForm.propTypes = {
115
+ grantRequest: PropTypes.object,
116
+ isRejection: PropTypes.bool,
117
+ createGrantRequestApproval: PropTypes.func,
118
+ saving: PropTypes.bool,
119
+ };
120
+
121
+ export const mapStateToProps = (state) => ({
122
+ saving: state?.creatingGrantRequestApproval,
123
+ });
124
+
125
+ export default connect(mapStateToProps, { createGrantRequestApproval })(
126
+ GrantRequestApprovalForm
127
+ );
@@ -0,0 +1,43 @@
1
+ import _ from "lodash/fp";
2
+ import React, { useState } from "react";
3
+ import { Button, Popup } from "semantic-ui-react";
4
+ import PropTypes from "prop-types";
5
+ import GrantRequestApprovalForm from "./GrantRequestApprovalForm";
6
+
7
+ export const GrantRequestApprovalPopup = ({
8
+ isRejection,
9
+ grantRequest,
10
+ ...buttonProps
11
+ }) => {
12
+ const [open, setOpen] = useState(false);
13
+
14
+ return (
15
+ <Popup
16
+ on="click"
17
+ basic
18
+ flowing
19
+ content={
20
+ <GrantRequestApprovalForm
21
+ isRejection={isRejection}
22
+ grantRequest={grantRequest}
23
+ />
24
+ }
25
+ onOpen={() => setOpen(true)}
26
+ onClose={() => setOpen(false)}
27
+ open={open}
28
+ position="bottom right"
29
+ size="large"
30
+ positionFixed
31
+ trigger={
32
+ <Button className="button icon group-actions" {...buttonProps} />
33
+ }
34
+ />
35
+ );
36
+ };
37
+
38
+ GrantRequestApprovalPopup.propTypes = {
39
+ grantRequest: PropTypes.object,
40
+ isRejection: PropTypes.bool,
41
+ saving: PropTypes.bool,
42
+ };
43
+ export default GrantRequestApprovalPopup;
@@ -0,0 +1,41 @@
1
+ import React from "react";
2
+ import { connect } from "react-redux";
3
+ import PropTypes from "prop-types";
4
+ import { useIntl } from "react-intl";
5
+ import { Header, Icon, Segment } from "semantic-ui-react";
6
+ import {
7
+ getGrantRequests,
8
+ getGrantRequestsApprovalColumns
9
+ } from "../selectors";
10
+ import GrantRequests from "./GrantRequests";
11
+
12
+ export const GrantRequestApprovals = ({ columns, grantRequests }) => {
13
+ const { formatMessage } = useIntl();
14
+
15
+ return (
16
+ <Segment>
17
+ <Header as="h2">
18
+ <Icon circular name="key" />
19
+ <Header.Content>
20
+ {formatMessage({ id: "grantRequests.header.approve" })}
21
+ <Header.Subheader>
22
+ {formatMessage({ id: "grantRequests.subheader.approve" })}
23
+ </Header.Subheader>
24
+ </Header.Content>
25
+ </Header>
26
+ <GrantRequests columns={columns} grantRequests={grantRequests} />
27
+ </Segment>
28
+ );
29
+ };
30
+
31
+ const mapStateToProps = state => ({
32
+ columns: getGrantRequestsApprovalColumns(state),
33
+ grantRequests: getGrantRequests(state)
34
+ });
35
+
36
+ GrantRequestApprovals.propTypes = {
37
+ columns: PropTypes.array,
38
+ grantRequests: PropTypes.array
39
+ };
40
+
41
+ export default connect(mapStateToProps)(GrantRequestApprovals);
@@ -4,13 +4,15 @@ import PropTypes from "prop-types";
4
4
  import { Route, Switch } from "react-router-dom";
5
5
  import { connect } from "react-redux";
6
6
  import {
7
+ GRANT_REQUEST_APPROVALS,
7
8
  GRANT_REQUEST,
8
9
  GRANT_REQUESTS,
9
10
  GRANTS_REQUESTS_CHECKOUT,
10
11
  GRANTS,
11
12
  MY_GRANT_REQUESTS,
12
- MY_GRANTS,
13
+ MY_GRANTS
13
14
  } from "@truedat/core/routes";
15
+ import GrantRequestApprovals from "./GrantRequestApprovals";
14
16
  import StructureGrantCartCheckout from "./StructureGrantCartCheckout";
15
17
  import GrantFiltersLoader from "./GrantFiltersLoader";
16
18
  import GrantRequestsLoader from "./GrantRequestsLoader";
@@ -50,6 +52,20 @@ export const GrantRoutes = ({ grantRequestLoaded }) => (
50
52
  </>
51
53
  )}
52
54
  />
55
+ <Route
56
+ path={GRANT_REQUEST_APPROVALS}
57
+ render={() => (
58
+ <>
59
+ <GrantRequestsLoader
60
+ defaultPayload={{
61
+ action: "approve",
62
+ status: "pending"
63
+ }}
64
+ />
65
+ <GrantRequestApprovals />
66
+ </>
67
+ )}
68
+ />
53
69
  <Route
54
70
  path={MY_GRANT_REQUESTS}
55
71
  render={() => (
@@ -57,7 +73,7 @@ export const GrantRoutes = ({ grantRequestLoaded }) => (
57
73
  <GrantRequestsLoader
58
74
  defaultPayload={{
59
75
  user: "me",
60
- status: "pending,approved,rejected,processing",
76
+ status: "pending,approved,rejected,processing"
61
77
  }}
62
78
  />
63
79
  <MyGrantRequests />
@@ -86,12 +102,12 @@ export const GrantRoutes = ({ grantRequestLoaded }) => (
86
102
  </>
87
103
  );
88
104
 
89
- const mapStateToProps = (state) => ({
90
- grantRequestLoaded: !_.isEmpty(state.grantRequest),
105
+ const mapStateToProps = state => ({
106
+ grantRequestLoaded: !_.isEmpty(state.grantRequest)
91
107
  });
92
108
 
93
109
  GrantRoutes.propTypes = {
94
- grantRequestLoaded: PropTypes.bool,
110
+ grantRequestLoaded: PropTypes.bool
95
111
  };
96
112
 
97
113
  export default connect(mapStateToProps)(GrantRoutes);
@@ -5,7 +5,7 @@ import PropTypes from "prop-types";
5
5
  import { useIntl } from "react-intl";
6
6
  import { Checkbox, Form, Header, Icon, Segment } from "semantic-ui-react";
7
7
  import { useForm, Controller } from "react-hook-form";
8
- import { getMyGrantRequests, getMyGrantRequestsColumns } from "../selectors";
8
+ import { getGrantRequests, getMyGrantRequestsColumns } from "../selectors";
9
9
  import { setGrantRequestsQuery } from "../routines";
10
10
  import GrantRequests from "./GrantRequests";
11
11
 
@@ -78,7 +78,7 @@ export const MyGrantRequests = ({
78
78
 
79
79
  const mapStateToProps = (state) => ({
80
80
  columns: getMyGrantRequestsColumns(state),
81
- grantRequests: getMyGrantRequests(state),
81
+ grantRequests: getGrantRequests(state),
82
82
  });
83
83
 
84
84
  MyGrantRequests.propTypes = {
@@ -20,7 +20,6 @@ import { GRANTS_REQUESTS_CHECKOUT, STRUCTURES } from "@truedat/core/routes";
20
20
  import { FiltersGrid } from "@truedat/dq/components/ruleImplementationForm/FiltersGrid";
21
21
  import { getRuleImplementationOperators } from "@truedat/dq/selectors/getRuleImplementationOperators";
22
22
  import {
23
- addGrantRequestToCart,
24
23
  removeGrantRequestFromCart,
25
24
  updateGrantRequestFilter,
26
25
  } from "../routines";
@@ -127,7 +126,7 @@ export const StructureGrantCart = ({
127
126
  <>
128
127
  <Divider />
129
128
  <Button as={Link} to={linkTo.GRANTS_REQUESTS_CHECKOUT()}>
130
- Checkout
129
+ {formatMessage({ id: `structure.grant.cart.checkout` })}
131
130
  </Button>
132
131
  </>
133
132
  )}
@@ -139,7 +138,6 @@ StructureGrantCart.propTypes = {
139
138
  structure: PropTypes.object,
140
139
  grantRequestsCart: PropTypes.object,
141
140
  typeOperators: PropTypes.array,
142
- addGrantRequestToCart: PropTypes.func,
143
141
  removeGrantRequestFromCart: PropTypes.func,
144
142
  updateGrantRequestFilter: PropTypes.func,
145
143
  };
@@ -156,7 +154,6 @@ const mapStateToProps = (state) => {
156
154
  const mapDispatchToProps = (dispatch) =>
157
155
  bindActionCreators(
158
156
  {
159
- addGrantRequestToCart,
160
157
  removeGrantRequestFromCart,
161
158
  updateGrantRequestFilter,
162
159
  },
@@ -0,0 +1,87 @@
1
+ import React from "react";
2
+ import { waitFor } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { render } from "@truedat/test/render";
5
+ import { GrantRequestApprovalForm } from "../GrantRequestApprovalForm";
6
+
7
+ describe("<GrantRequestApprovalForm />", () => {
8
+ const grantRequest = {
9
+ id: 3,
10
+ domain_id: 8,
11
+ pending_roles: ["role1", "role2"],
12
+ };
13
+ const createGrantRequestApproval = jest.fn();
14
+ const saving = false;
15
+ const props = {
16
+ grantRequest,
17
+ isRejection: false,
18
+ saving,
19
+ createGrantRequestApproval,
20
+ };
21
+ const renderOpts = {
22
+ messages: {
23
+ en: {
24
+ "grantRequestApproval.form.approve": "approve",
25
+ "grantRequestApproval.form.reject": "reject",
26
+ "grantRequestApproval.form.role.label": "role_label",
27
+ "grantRequestApproval.form.role.placeholder": "role_placeholder",
28
+ "grantRequestApproval.form.comments.label": "comments_label",
29
+ "grantRequestApproval.form.comments.placeholder":
30
+ "comments_placeholder",
31
+ "grantRequest.actions.approve": "submit",
32
+ "grantRequest.actions.reject": "submit",
33
+ },
34
+ },
35
+ };
36
+
37
+ it("matches the latest snapshot", () => {
38
+ const { container } = render(
39
+ <GrantRequestApprovalForm {...props} />,
40
+ renderOpts
41
+ );
42
+ expect(container).toMatchSnapshot();
43
+ });
44
+
45
+ it("enables submit button when we select a role", async () => {
46
+ const { findByText, queryByText } = render(
47
+ <GrantRequestApprovalForm {...props} />,
48
+ renderOpts
49
+ );
50
+
51
+ expect(queryByText(/submit/)).toBeDisabled();
52
+ userEvent.click(await findByText(/role1/));
53
+ await waitFor(() => {
54
+ expect(queryByText(/submit/)).not.toBeDisabled();
55
+ });
56
+ });
57
+
58
+ it("invokes function when submit button clicked", async () => {
59
+ const { findByText, queryByText, queryByTestId } = render(
60
+ <GrantRequestApprovalForm {...props} />,
61
+ renderOpts
62
+ );
63
+
64
+ expect(queryByText(/submit/)).toBeDisabled();
65
+ userEvent.click(await findByText(/role1/));
66
+ await waitFor(() => {
67
+ expect(queryByText(/submit/)).not.toBeDisabled();
68
+ });
69
+
70
+ userEvent.type(queryByTestId("comments-id"), "comment");
71
+ userEvent.click(await findByText(/submit/));
72
+ await waitFor(() => {
73
+ expect(createGrantRequestApproval.mock.calls.length).toBe(1);
74
+ });
75
+ expect(createGrantRequestApproval).toHaveBeenCalledWith(
76
+ expect.objectContaining({
77
+ grantRequestId: 3,
78
+ approval: {
79
+ domain_id: 8,
80
+ is_rejection: false,
81
+ role: "role1",
82
+ comments: "comment",
83
+ },
84
+ })
85
+ );
86
+ });
87
+ });
@@ -0,0 +1,16 @@
1
+ import React from "react";
2
+ import { shallow } from "enzyme";
3
+ import GrantRequestApprovalPopup from "../GrantRequestApprovalPopup";
4
+
5
+ describe("<GrantRequestApprovalPopup />", () => {
6
+ const props = {
7
+ isRejection: false,
8
+ grantRequest: { id: 2 },
9
+ content: "button content",
10
+ primary: true,
11
+ };
12
+ it("matches the latest snapshot", () => {
13
+ const wrapper = shallow(<GrantRequestApprovalPopup {...props} />);
14
+ expect(wrapper).toMatchSnapshot();
15
+ });
16
+ });
@@ -47,7 +47,18 @@ exports[`<GrantRequest /> matches the latest snapshot 1`] = `
47
47
  </div>
48
48
  <div
49
49
  class="right aligned four wide column"
50
- />
50
+ >
51
+ <button
52
+ class="ui primary button button icon group-actions"
53
+ >
54
+ grantRequest.actions.approve
55
+ </button>
56
+ <button
57
+ class="ui secondary button button icon group-actions"
58
+ >
59
+ grantRequest.actions.reject
60
+ </button>
61
+ </div>
51
62
  </div>
52
63
  <div
53
64
  class="ui big very relaxed list"
@@ -0,0 +1,101 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`<GrantRequestApprovalForm /> matches the latest snapshot 1`] = `
4
+ <div>
5
+ <div
6
+ class="ui container"
7
+ style="width: 400px;"
8
+ >
9
+ <h2
10
+ class="ui header"
11
+ >
12
+ approve
13
+ </h2>
14
+ <form
15
+ class="ui form"
16
+ >
17
+ <div
18
+ class="required field"
19
+ >
20
+ <label>
21
+ role_label
22
+ </label>
23
+ <div
24
+ aria-expanded="false"
25
+ class="ui selection dropdown"
26
+ required=""
27
+ role="listbox"
28
+ tabindex="0"
29
+ >
30
+ <div
31
+ aria-atomic="true"
32
+ aria-live="polite"
33
+ class="divider default text"
34
+ role="alert"
35
+ >
36
+ role_placeholder
37
+ </div>
38
+ <i
39
+ aria-hidden="true"
40
+ class="dropdown icon"
41
+ />
42
+ <div
43
+ class="menu transition"
44
+ >
45
+ <div
46
+ aria-checked="false"
47
+ aria-selected="true"
48
+ class="selected item"
49
+ role="option"
50
+ style="pointer-events: all;"
51
+ >
52
+ <span
53
+ class="text"
54
+ >
55
+ role1
56
+ </span>
57
+ </div>
58
+ <div
59
+ aria-checked="false"
60
+ aria-selected="false"
61
+ class="item"
62
+ role="option"
63
+ style="pointer-events: all;"
64
+ >
65
+ <span
66
+ class="text"
67
+ >
68
+ role2
69
+ </span>
70
+ </div>
71
+ </div>
72
+ </div>
73
+ </div>
74
+ <div
75
+ class="field"
76
+ >
77
+ <label>
78
+ comments_label
79
+ </label>
80
+ <textarea
81
+ data-testid="comments-id"
82
+ placeholder="comments_placeholder"
83
+ rows="3"
84
+ />
85
+ </div>
86
+ <div
87
+ class="actions"
88
+ >
89
+ <button
90
+ class="ui primary disabled button"
91
+ disabled=""
92
+ tabindex="-1"
93
+ type="submit"
94
+ >
95
+ submit
96
+ </button>
97
+ </div>
98
+ </form>
99
+ </div>
100
+ </div>
101
+ `;
@@ -0,0 +1,37 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`<GrantRequestApprovalPopup /> matches the latest snapshot 1`] = `
4
+ <Popup
5
+ basic={true}
6
+ content={
7
+ <Memo(Connect(GrantRequestApprovalForm))
8
+ grantRequest={
9
+ Object {
10
+ "id": 2,
11
+ }
12
+ }
13
+ isRejection={false}
14
+ />
15
+ }
16
+ disabled={false}
17
+ eventsEnabled={true}
18
+ flowing={true}
19
+ on="click"
20
+ onClose={[Function]}
21
+ onOpen={[Function]}
22
+ open={false}
23
+ pinned={false}
24
+ popperModifiers={Array []}
25
+ position="bottom right"
26
+ positionFixed={true}
27
+ size="large"
28
+ trigger={
29
+ <Button
30
+ as="button"
31
+ className="button icon group-actions"
32
+ content="button content"
33
+ primary={true}
34
+ />
35
+ }
36
+ />
37
+ `;
@@ -14,6 +14,10 @@ exports[`<GrantRoutes /> matches the latest snapshot 1`] = `
14
14
  path="/my_grants"
15
15
  render={[Function]}
16
16
  />
17
+ <Route
18
+ path="/grant_request_approvals"
19
+ render={[Function]}
20
+ />
17
21
  <Route
18
22
  path="/my_grant_requests"
19
23
  render={[Function]}
@@ -38,6 +38,8 @@ export default {
38
38
  "grant.header.end_date": "Expires",
39
39
  "grant.header.start_date": "Access granted",
40
40
  "grant.updated_at": "Last changed",
41
+ "grantRequest.actions.approve": "Approve",
42
+ "grantRequest.actions.reject": "Reject",
41
43
  "grantRequest.content.approved": "Approved",
42
44
  "grantRequest.content.empty": "Empty",
43
45
  "grantRequest.content.pending": "Pending",
@@ -45,18 +47,26 @@ export default {
45
47
  "grantRequest.content.processing": "Processing",
46
48
  "grantRequest.content.rejected": "Rejected",
47
49
  "grantRequest.crumb.title": "Request",
48
- "grantRequest.header": "Grant request",
49
- "grantRequests.empty": "No grant requests found",
50
- "grantRequests.header": "Grant requests",
51
50
  "grantRequest.header.data_structure": "Data Structure",
52
51
  "grantRequest.header.path": "Path",
52
+ "grantRequest.header.status": "Status",
53
53
  "grantRequest.header.updated_at": "Updated",
54
54
  "grantRequest.header.user": "User",
55
- "grantRequest.header.status": "Status",
55
+ "grantRequest.header": "Grant request",
56
+ "grantRequestApproval.form.approve": "Approve grant request",
57
+ "grantRequestApproval.form.comments.label": "Comments",
58
+ "grantRequestApproval.form.comments.placeholder": "Comments",
59
+ "grantRequestApproval.form.reject": "Reject grant request",
60
+ "grantRequestApproval.form.role.label": "Approval role",
61
+ "grantRequestApproval.form.role.placeholder": "Role",
62
+ "grantRequests.empty": "No grant requests found",
56
63
  "grantRequests.filters": "Filters",
64
+ "grantRequests.header.approve": "Approve grant requests",
57
65
  "grantRequests.header.me": "My grant requests",
58
- "grantRequests.subheader": "Pending approval grant requests",
66
+ "grantRequests.header": "Grant requests",
67
+ "grantRequests.subheader.approve": "Grant requests pending approval",
59
68
  "grantRequests.subheader.me": "Grant requests requested by me",
69
+ "grantRequests.subheader": "Pending approval grant requests",
60
70
  "grants.actions.download.popup": "Select a grant",
61
71
  "grants.actions.download.tooltip": "Download to CSV",
62
72
  "grants.header": "Grants",
@@ -163,6 +173,7 @@ export default {
163
173
  "structure.grant.request_grant": "Request grant",
164
174
  "structure.grant.requested_grant": "Pending request",
165
175
  "structure.grant.cart": "Requested grants",
176
+ "structure.grant.cart.checkout": "Checkout",
166
177
  "structure.grant.cart.template": "Request information",
167
178
  "structure.grant.cart.template.placeholder": "Select request type",
168
179
  "structure.grant.cart.filters": "Filters for structure",
@@ -39,6 +39,8 @@ export default {
39
39
  "grant.header.start_date": "Acceso desde",
40
40
  "grant.header.user": "Cuenta",
41
41
  "grant.updated_at": "Actualizada",
42
+ "grantRequest.actions.approve": "Aprobar",
43
+ "grantRequest.actions.reject": "Rechazar",
42
44
  "grantRequest.content.approved": "Aprobada",
43
45
  "grantRequest.content.empty": "Vacío",
44
46
  "grantRequest.content.pending": "Pendiente",
@@ -46,18 +48,27 @@ export default {
46
48
  "grantRequest.content.processing": "Procesando",
47
49
  "grantRequest.content.rejected": "Rechazada",
48
50
  "grantRequest.crumb.title": "Petición",
49
- "grantRequest.header": "Petición de acceso",
50
- "grantRequests.empty": "No se han encontrado peticiones de datos",
51
- "grantRequests.header": "Peticiones de acceso",
52
51
  "grantRequest.header.data_structure": "Estructura",
53
52
  "grantRequest.header.path": "Ruta",
53
+ "grantRequest.header.status": "Estado",
54
54
  "grantRequest.header.updated_at": "Actualizado",
55
55
  "grantRequest.header.user": "Usuario",
56
- "grantRequest.header.status": "Estado",
57
- "grantRequests.header.me": "Mis peticiones de acceso",
56
+ "grantRequest.header": "Petición de acceso",
57
+ "grantRequestApproval.form.approve": "Aprobar petición de acceso",
58
+ "grantRequestApproval.form.comments.label": "Comentarios",
59
+ "grantRequestApproval.form.comments.placeholder": "Comentarios",
60
+ "grantRequestApproval.form.reject": "Rechazar petición de acceso",
61
+ "grantRequestApproval.form.role.label": "Rol de aprobación",
62
+ "grantRequestApproval.form.role.placeholder": "Rol",
63
+ "grantRequests.empty": "No se han encontrado peticiones de datos",
58
64
  "grantRequests.filters": "Filtros",
59
- "grantRequests.subheader": "Peticiones de acceso pendientes de aprobación",
65
+ "grantRequests.header.approve": "Aprobar peticiones de acceso",
66
+ "grantRequests.header.me": "Mis peticiones de acceso",
67
+ "grantRequests.header": "Peticiones de acceso",
68
+ "grantRequests.subheader.approve":
69
+ "Peticiones de acceso pendientes de aprobación",
60
70
  "grantRequests.subheader.me": "Peticiones de acceso demandadas por mí",
71
+ "grantRequests.subheader": "Peticiones de acceso pendientes de aprobación",
61
72
  "grants.actions.download.popup": "Seleccione un acceso",
62
73
  "grants.actions.download.tooltip": "Descargar a CSV",
63
74
  "grants.header": "Accesos",
@@ -165,6 +176,7 @@ export default {
165
176
  "structure.grant.request_grant": "Solicitar acceso",
166
177
  "structure.grant.requested_grant": "Solicitud pendiente",
167
178
  "structure.grant.cart": "Accesos a solicitar",
179
+ "structure.grant.cart.checkout": "Checkout",
168
180
  "structure.grant.cart.template": "Información de solicitud",
169
181
  "structure.grant.cart.template.placeholder": "Seleccione tipo de solicitud",
170
182
  "structure.grant.cart.filters": "Filtros para la estructura",
@@ -0,0 +1,33 @@
1
+ import { createGrantRequestApproval } from "../../routines";
2
+ import { creatingGrantRequestApproval } from "../creatingGrantRequestApproval";
3
+
4
+ const fooState = { foo: "bar" };
5
+ const initialState = false;
6
+
7
+ describe("reducers: creatingGrantRequestApproval", () => {
8
+ it("should provide the initial state", () => {
9
+ expect(creatingGrantRequestApproval(undefined, {})).toBe(initialState);
10
+ });
11
+
12
+ it("should handle the createGrantRequestApproval.TRIGGER action", () => {
13
+ expect(
14
+ creatingGrantRequestApproval(fooState, {
15
+ type: createGrantRequestApproval.TRIGGER,
16
+ })
17
+ ).toBe(true);
18
+ });
19
+
20
+ it("should handle the createGrantRequestApproval.FULFILL action", () => {
21
+ expect(
22
+ creatingGrantRequestApproval(fooState, {
23
+ type: createGrantRequestApproval.FULFILL,
24
+ })
25
+ ).toBe(initialState);
26
+ });
27
+
28
+ it("should ignore unknown actions", () => {
29
+ expect(creatingGrantRequestApproval(fooState, { type: "FOO" })).toBe(
30
+ fooState
31
+ );
32
+ });
33
+ });
@@ -0,0 +1,16 @@
1
+ import { createGrantRequestApproval } from "../routines";
2
+
3
+ const initialState = false;
4
+
5
+ const creatingGrantRequestApproval = (state = initialState, { type }) => {
6
+ switch (type) {
7
+ case createGrantRequestApproval.TRIGGER:
8
+ return true;
9
+ case createGrantRequestApproval.FULFILL:
10
+ return initialState;
11
+ default:
12
+ return state;
13
+ }
14
+ };
15
+
16
+ export { creatingGrantRequestApproval };
@@ -1,5 +1,6 @@
1
1
  import { bulkUpdateStructuresLoading } from "./bulkUpdateStructuresLoading";
2
2
  import { childrenRelations, parentRelations } from "./structureRelations";
3
+ import { creatingGrantRequestApproval } from "./creatingGrantRequestApproval";
3
4
  import { csvGraphDownloading } from "./csvGraphDownloading";
4
5
  import { ddMessage } from "./ddMessage";
5
6
  import { deletingStructureTags } from "./deletingStructureTags";
@@ -91,6 +92,7 @@ import { grantRequestsQuery } from "./grantRequestsQuery";
91
92
  export {
92
93
  bulkUpdateStructuresLoading,
93
94
  childrenRelations,
95
+ creatingGrantRequestApproval,
94
96
  csvGraphDownloading,
95
97
  ddMessage,
96
98
  deletingStructureTags,
@@ -15,6 +15,7 @@ import {
15
15
  uploadStructures,
16
16
  doStructureNoteAction,
17
17
  checkoutGrantRequest,
18
+ createGrantRequestApproval,
18
19
  } from "../routines";
19
20
 
20
21
  export const initialState = "";
@@ -67,6 +68,9 @@ export const structureRedirect = (
67
68
  const id = _.prop("data.id")(payload);
68
69
  return linkTo.PROFILE_GROUP({ id });
69
70
  }
71
+ case createGrantRequestApproval.SUCCESS: {
72
+ return linkTo.GRANT_REQUEST_APPROVALS();
73
+ }
70
74
  default:
71
75
  return state;
72
76
  }
package/src/routines.js CHANGED
@@ -156,3 +156,7 @@ export const removeGrantRequestsQuery = createRoutine(
156
156
  "REMOVE_GRANT_REQUESTS_QUERY"
157
157
  );
158
158
  export const setGrantRequestsQuery = createRoutine("SET_GRANT_REQUESTS_QUERY");
159
+
160
+ export const createGrantRequestApproval = createRoutine(
161
+ "CREATE_GRANT_REQUEST_APPROVAL"
162
+ );
@@ -0,0 +1,80 @@
1
+ import { compile } from "path-to-regexp";
2
+ import { testSaga } from "redux-saga-test-plan";
3
+ import { apiJsonPost, JSON_OPTS } from "@truedat/core/services/api";
4
+ import {
5
+ createGrantRequestApprovalRequestSaga,
6
+ createGrantRequestApprovalSaga,
7
+ } from "../createGrantRequestApproval";
8
+ import { createGrantRequestApproval } from "../../routines";
9
+ import { API_GRANT_REQUEST_APPROVALS } from "../../api";
10
+
11
+ describe("sagas: createGrantRequestApprovalRequestSaga", () => {
12
+ it("should invoke createGrantRequestApprovalSaga on createGrantRequestApproval.TRIGGER", () => {
13
+ expect(() => {
14
+ testSaga(createGrantRequestApprovalRequestSaga)
15
+ .next()
16
+ .takeLatest(
17
+ createGrantRequestApproval.TRIGGER,
18
+ createGrantRequestApprovalSaga
19
+ )
20
+ .finish()
21
+ .isDone();
22
+ }).not.toThrow();
23
+ });
24
+
25
+ it("should throw exception if an unhandled action is received", () => {
26
+ expect(() => {
27
+ testSaga(createGrantRequestApprovalRequestSaga)
28
+ .next()
29
+ .takeLatest("FOO", createGrantRequestApprovalRequestSaga);
30
+ }).toThrow();
31
+ });
32
+ });
33
+
34
+ describe("sagas: createGrantRequestApprovalSaga", () => {
35
+ const approval = {
36
+ body: "approval",
37
+ };
38
+ const payload = {
39
+ grantRequestId: 8,
40
+ approval,
41
+ };
42
+ const request_data = { approval };
43
+
44
+ const url = compile(API_GRANT_REQUEST_APPROVALS)({ id: 8 });
45
+
46
+ it("should put a success action when a response is returned", () => {
47
+ expect(() => {
48
+ testSaga(createGrantRequestApprovalSaga, { payload })
49
+ .next()
50
+ .put({ ...createGrantRequestApproval.request(request_data) })
51
+ .next()
52
+ .call(apiJsonPost, url, request_data, JSON_OPTS)
53
+ .next({ data: payload })
54
+ .put({ ...createGrantRequestApproval.success(payload) })
55
+ .next()
56
+ .put(createGrantRequestApproval.fulfill())
57
+ .next()
58
+ .isDone();
59
+ }).not.toThrow();
60
+ });
61
+
62
+ it("should put a failure action when the call returns an error", () => {
63
+ const message = "Request failed";
64
+ const error = { message };
65
+
66
+ expect(() => {
67
+ testSaga(createGrantRequestApprovalSaga, { payload })
68
+ .next()
69
+ .put({ ...createGrantRequestApproval.request(request_data) })
70
+ .next()
71
+ .call(apiJsonPost, url, request_data, JSON_OPTS)
72
+ .throw(error)
73
+ .put(createGrantRequestApproval.failure(message))
74
+ .next()
75
+ .put(createGrantRequestApproval.fulfill())
76
+ .next()
77
+ .isDone();
78
+ }).not.toThrow();
79
+ });
80
+ });
@@ -0,0 +1,31 @@
1
+ import { compile } from "path-to-regexp";
2
+ import { call, put, takeLatest } from "redux-saga/effects";
3
+ import { apiJsonPost, JSON_OPTS } from "@truedat/core/services/api";
4
+ import { createGrantRequestApproval } from "../routines";
5
+ import { API_GRANT_REQUEST_APPROVALS } from "../api";
6
+
7
+ export function* createGrantRequestApprovalSaga({ payload }) {
8
+ try {
9
+ const { grantRequestId, approval } = payload;
10
+ const url = compile(API_GRANT_REQUEST_APPROVALS)({ id: grantRequestId });
11
+ yield put(createGrantRequestApproval.request({ approval }));
12
+ const { data } = yield call(apiJsonPost, url, { approval }, JSON_OPTS);
13
+ yield put(createGrantRequestApproval.success(data));
14
+ } catch (error) {
15
+ if (error.response) {
16
+ const { status, data } = error.response;
17
+ yield put(createGrantRequestApproval.failure({ status, data }));
18
+ } else {
19
+ yield put(createGrantRequestApproval.failure(error.message));
20
+ }
21
+ } finally {
22
+ yield put(createGrantRequestApproval.fulfill());
23
+ }
24
+ }
25
+
26
+ export function* createGrantRequestApprovalRequestSaga() {
27
+ yield takeLatest(
28
+ createGrantRequestApproval.TRIGGER,
29
+ createGrantRequestApprovalSaga
30
+ );
31
+ }
@@ -1,4 +1,5 @@
1
1
  import { bulkUpdateStructuresRequestsSaga } from "./bulkUpdateStructures";
2
+ import { createGrantRequestApprovalRequestSaga } from "./createGrantRequestApproval";
2
3
  import { createGraphRequestSaga } from "./createGraph";
3
4
  import { createProfileGroupRequestSaga } from "./createProfileGroup";
4
5
  import { createStructureGraphRequestSaga } from "./createStructureGraph";
@@ -46,6 +47,7 @@ import { operateGrantRequestToCartRequestSaga } from "./operateGrantRequestCart"
46
47
 
47
48
  export {
48
49
  bulkUpdateStructuresRequestsSaga,
50
+ createGrantRequestApprovalRequestSaga,
49
51
  createGraphRequestSaga,
50
52
  createProfileGroupRequestSaga,
51
53
  createStructureGraphRequestSaga,
@@ -94,6 +96,7 @@ export {
94
96
 
95
97
  export default [
96
98
  bulkUpdateStructuresRequestsSaga(),
99
+ createGrantRequestApprovalRequestSaga(),
97
100
  createGraphRequestSaga(),
98
101
  createProfileGroupRequestSaga(),
99
102
  createStructureGraphRequestSaga(),
@@ -1,4 +1,4 @@
1
- import { getMyGrantRequests } from "..";
1
+ import { getGrantRequests } from "..";
2
2
 
3
3
  const grantRequests = [
4
4
  { updated_at: "2021-09-20 11:05:58", id: 1 },
@@ -6,12 +6,12 @@ const grantRequests = [
6
6
  { updated_at: "2021-10-04 07:25:01", id: 3 },
7
7
  ];
8
8
 
9
- describe("selectors: getMyGrantRequests", () => {
9
+ describe("selectors: getGrantRequests", () => {
10
10
  const state = {
11
11
  grantRequests,
12
12
  };
13
13
  it("get fields sorted by updated_at", () => {
14
- expect(getMyGrantRequests(state)).toEqual([
14
+ expect(getGrantRequests(state)).toEqual([
15
15
  { updated_at: "2021-10-04 07:25:01", id: 3 },
16
16
  { updated_at: "2021-09-20 11:05:58", id: 1 },
17
17
  { id: 2 },
@@ -1,7 +1,7 @@
1
1
  import _ from "lodash/fp";
2
2
  import { createSelector } from "reselect";
3
3
 
4
- export const getMyGrantRequests = createSelector(
4
+ export const getGrantRequests = createSelector(
5
5
  [_.prop("grantRequests")],
6
6
  (grantRequests) => {
7
7
  return _.orderBy(
@@ -66,7 +66,7 @@ export const defaultGrantRequestsTableColumns = [
66
66
  {
67
67
  name: "user",
68
68
  fieldSelector: (grantRequest) => ({
69
- user: grantRequest?._embedded?.group?.user,
69
+ user: grantRequest?._embedded?.group?._embedded?.user,
70
70
  }),
71
71
  fieldDecorator: UserDecorator,
72
72
  },
@@ -107,7 +107,17 @@ export const getMyGrantRequestsColumns = createSelector(
107
107
  (grantsRequestsColumns) => {
108
108
  return _.flow(
109
109
  _.defaultTo(defaultGrantRequestsTableColumns),
110
- _.reject((c) => c.name === "user")
110
+ _.reject({ name: "user" })
111
+ )(grantsRequestsColumns);
112
+ }
113
+ );
114
+
115
+ export const getGrantRequestsApprovalColumns = createSelector(
116
+ [_.prop("grantRequestsColumns")],
117
+ (grantsRequestsColumns) => {
118
+ return _.flow(
119
+ _.defaultTo(defaultGrantRequestsTableColumns),
120
+ _.reject({ name: "status" })
111
121
  )(grantsRequestsColumns);
112
122
  }
113
123
  );