@truedat/bg 4.38.4 → 4.38.8

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 (28) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/package.json +5 -5
  3. package/src/concepts/components/ConceptActions.js +1 -0
  4. package/src/concepts/components/ConceptHeader.js +5 -5
  5. package/src/concepts/components/ConceptManageDomain.js +87 -0
  6. package/src/concepts/components/ConceptManageDomainPopup.js +50 -0
  7. package/src/concepts/components/ConceptTaxonomy.js +11 -3
  8. package/src/concepts/components/__tests__/ConceptManageDomain.spec.js +43 -0
  9. package/src/concepts/components/__tests__/ConceptManageDomainPopup.spec.js +36 -0
  10. package/src/concepts/components/__tests__/__snapshots__/ConceptManageDomain.spec.js.snap +129 -0
  11. package/src/concepts/components/__tests__/__snapshots__/ConceptManageDomainPopup.spec.js.snap +171 -0
  12. package/src/concepts/components/__tests__/__snapshots__/ConceptTaxonomy.spec.js.snap +6 -0
  13. package/src/concepts/reducers/index.js +3 -1
  14. package/src/concepts/reducers/updatingDomain.js +16 -0
  15. package/src/messages/en.js +4 -0
  16. package/src/messages/es.js +4 -0
  17. package/src/taxonomy/components/DomainsConceptLoader.js +43 -0
  18. package/src/taxonomy/components/__tests__/DomainsConceptLoader.spec.js +38 -0
  19. package/src/taxonomy/reducers/__tests__/domainConceptLoading.spec.js +26 -0
  20. package/src/taxonomy/reducers/__tests__/domainsConcept.spec.js +30 -0
  21. package/src/taxonomy/reducers/domainsConcept.js +19 -0
  22. package/src/taxonomy/reducers/domainsConceptLoading.js +12 -0
  23. package/src/taxonomy/reducers/index.js +4 -0
  24. package/src/taxonomy/routines.js +2 -0
  25. package/src/taxonomy/sagas/__tests__/fetchDomainsConcept.spec.js +93 -0
  26. package/src/taxonomy/sagas/fetchDomainsConcept.js +37 -0
  27. package/src/taxonomy/sagas/index.js +3 -0
  28. package/src/taxonomy/selectors/getDomainsConcept.js +176 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [4.38.8] 2022-02-22
4
+
5
+ ### Added
6
+
7
+ - [TD-4481] Allow domain change on business concept
8
+
3
9
  ## [4.37.4] 2022-02-04
4
10
 
5
11
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/bg",
3
- "version": "4.38.4",
3
+ "version": "4.38.8",
4
4
  "description": "Truedat Web Business Glossary",
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.38.4",
37
+ "@truedat/test": "4.38.8",
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",
@@ -83,8 +83,8 @@
83
83
  ]
84
84
  },
85
85
  "dependencies": {
86
- "@truedat/core": "4.38.4",
87
- "@truedat/df": "4.38.4",
86
+ "@truedat/core": "4.38.8",
87
+ "@truedat/df": "4.38.8",
88
88
  "file-saver": "^2.0.5",
89
89
  "moment": "^2.24.0",
90
90
  "path-to-regexp": "^1.7.0",
@@ -104,5 +104,5 @@
104
104
  "react-dom": ">= 16.8.6 < 17",
105
105
  "semantic-ui-react": ">= 0.88.2 < 2.1"
106
106
  },
107
- "gitHead": "2e83dbac040e237b3f688782b40b6bd35edbd148"
107
+ "gitHead": "66f56f3fe3e1b5eb0b18379f2d141b1433bf82c1"
108
108
  }
@@ -228,6 +228,7 @@ const hiddenActions = [
228
228
  "upload",
229
229
  "bulk_update",
230
230
  "set_confidential",
231
+ "update_domain",
231
232
  ];
232
233
 
233
234
  const editUrl = (conceptActions, concept) =>
@@ -16,12 +16,12 @@ export const ConceptHeader = ({
16
16
  setConfidentialConcept,
17
17
  share,
18
18
  domain,
19
- template
19
+ template,
20
20
  }) => {
21
21
  const { formatMessage } = useIntl();
22
22
  const path = linkTo.CONCEPT_VERSION({
23
23
  business_concept_id: _.prop("business_concept_id")(concept),
24
- id: "current"
24
+ id: "current",
25
25
  });
26
26
  const name = concept?.name;
27
27
  const description = concept?.description;
@@ -78,15 +78,15 @@ ConceptHeader.propTypes = {
78
78
  share: PropTypes.bool,
79
79
  concept: PropTypes.object,
80
80
  domain: PropTypes.object,
81
- template: PropTypes.object
81
+ template: PropTypes.object,
82
82
  };
83
83
 
84
84
  const mapStateToProps = ({ concept, conceptActions, conceptPermissions }) => ({
85
- share: _.has("share")(conceptPermissions),
85
+ share: _.prop("share")(conceptPermissions),
86
86
  concept,
87
87
  setConfidentialConcept: _.prop("set_confidential")(conceptActions),
88
88
  domain: concept.domain || {},
89
- template: concept.template || {}
89
+ template: concept.template || {},
90
90
  });
91
91
 
92
92
  export default connect(mapStateToProps)(ConceptHeader);
@@ -0,0 +1,87 @@
1
+ import _ from "lodash/fp";
2
+ import React from "react";
3
+ import { Button, Container, Form, Header } from "semantic-ui-react";
4
+ import { connect } from "react-redux";
5
+ import { useForm, Controller } from "react-hook-form";
6
+ import { useIntl } from "react-intl";
7
+ import PropTypes from "prop-types";
8
+ import DomainDropdownSelector from "../../taxonomy/components/DomainDropdownSelector";
9
+ import { getDomainConceptSelectorOptions } from "../../taxonomy/selectors/getDomainsConcept";
10
+ import { conceptAction } from "../routines";
11
+
12
+ const actionKey = "update_domain";
13
+
14
+ export const ConceptManageDomain = ({
15
+ domainOptions,
16
+ conceptAction,
17
+ action,
18
+ setToDomain,
19
+ }) => {
20
+ const { formatMessage } = useIntl();
21
+ const { handleSubmit, control, formState } = useForm({
22
+ mode: "all",
23
+ defaultValues: {
24
+ setToDomain: _.prop("id")(setToDomain),
25
+ },
26
+ });
27
+ const { isDirty, isValid } = formState;
28
+ const onSubmit = ({ setToDomain }) => {
29
+ conceptAction({
30
+ action: actionKey,
31
+ ...action,
32
+ domain_id: setToDomain,
33
+ });
34
+ };
35
+
36
+ return (
37
+ <Container style={{ width: "400px" }}>
38
+ <Header
39
+ as="h2"
40
+ content={formatMessage({
41
+ id: "concept.changeDomain.header",
42
+ })}
43
+ />
44
+ <Form onSubmit={handleSubmit(onSubmit)}>
45
+ <Controller
46
+ control={control}
47
+ name="setToDomain"
48
+ render={({ onBlur, onChange, value }, { invalid }) => (
49
+ <DomainDropdownSelector
50
+ domainOptions={domainOptions}
51
+ name="setToDomain"
52
+ clearable={false}
53
+ error={invalid}
54
+ label={formatMessage({ id: "concept.changeDomain.label" })}
55
+ onBlur={onBlur}
56
+ required
57
+ onChange={(_e, { value }) => onChange(value)}
58
+ value={value}
59
+ />
60
+ )}
61
+ />
62
+ <div className="actions">
63
+ <Button
64
+ type="submit"
65
+ disabled={!isDirty || !isValid}
66
+ primary
67
+ content={formatMessage({ id: "conceptDomain.actions.update" })}
68
+ />
69
+ </div>
70
+ </Form>
71
+ </Container>
72
+ );
73
+ };
74
+
75
+ ConceptManageDomain.propTypes = {
76
+ domainOptions: PropTypes.array,
77
+ conceptAction: PropTypes.func,
78
+ setToDomain: PropTypes.array,
79
+ };
80
+
81
+ const mapStateToProps = (state) => ({
82
+ domainOptions: getDomainConceptSelectorOptions(state),
83
+ action: _.prop(actionKey)(state.conceptActions),
84
+ setToDomain: state.concept.domain,
85
+ });
86
+
87
+ export default connect(mapStateToProps, { conceptAction })(ConceptManageDomain);
@@ -0,0 +1,50 @@
1
+ import _ from "lodash/fp";
2
+ import React, { useState, useEffect } from "react";
3
+ import { Button, Icon, Popup } from "semantic-ui-react";
4
+ import { connect } from "react-redux";
5
+ import PropTypes from "prop-types";
6
+ import ConceptManageDomain from "./ConceptManageDomain";
7
+
8
+ export const ConceptManageDomainPopup = ({ update_domain, saving }) => {
9
+ const [open, setOpen] = useState(false);
10
+
11
+ useEffect(() => {
12
+ if (saving === false) {
13
+ setOpen(false);
14
+ }
15
+ }, [saving]);
16
+
17
+ return update_domain ? (
18
+ <Popup
19
+ on="click"
20
+ basic
21
+ flowing
22
+ content={<ConceptManageDomain />}
23
+ onOpen={() => setOpen(true)}
24
+ onClose={() => setOpen(false)}
25
+ open={open}
26
+ position="bottom right"
27
+ size="large"
28
+ positionFixed
29
+ trigger={
30
+ <Button
31
+ basic
32
+ icon={<Icon name={"pencil alternate"} />}
33
+ style={{ boxShadow: "none" }}
34
+ />
35
+ }
36
+ />
37
+ ) : null;
38
+ };
39
+
40
+ ConceptManageDomainPopup.propTypes = {
41
+ update_domain: PropTypes.bool,
42
+ saving: PropTypes.bool,
43
+ };
44
+
45
+ export const mapStateToProps = ({
46
+ conceptActions,
47
+ updatingDomain: saving,
48
+ }) => ({ update_domain: _.has("update_domain")(conceptActions), saving });
49
+
50
+ export default connect(mapStateToProps)(ConceptManageDomainPopup);
@@ -6,6 +6,11 @@ import { List, Header, Segment } from "semantic-ui-react";
6
6
  import { FormattedMessage } from "react-intl";
7
7
  import { getConceptDomainPath } from "../selectors";
8
8
  import DomainItem from "../../taxonomy/components/DomainItem";
9
+ import ConceptManageDomainPopup from "./ConceptManageDomainPopup";
10
+
11
+ const DomainsConceptLoader = React.lazy(() =>
12
+ import("../../taxonomy/components/DomainsConceptLoader")
13
+ );
9
14
 
10
15
  export const ConceptTaxonomy = ({ domainPath }) =>
11
16
  _.negate(_.isEmpty)(domainPath) && (
@@ -13,6 +18,9 @@ export const ConceptTaxonomy = ({ domainPath }) =>
13
18
  <Header as="h3" dividing>
14
19
  <FormattedMessage id="concepts.taxonomy" defaultMessage="Taxonomía" />
15
20
  </Header>
21
+ <DomainsConceptLoader actions="manage_business_concepts_domain" />
22
+ <ConceptManageDomainPopup conceptAction="concepts.actions.edit" />
23
+
16
24
  <List horizontal>
17
25
  {_.map.convert({ cap: false })((domain, i) => (
18
26
  <DomainItem
@@ -27,11 +35,11 @@ export const ConceptTaxonomy = ({ domainPath }) =>
27
35
  );
28
36
 
29
37
  ConceptTaxonomy.propTypes = {
30
- domainPath: PropTypes.array
38
+ domainPath: PropTypes.array,
31
39
  };
32
40
 
33
- const mapStateToProps = state => ({
34
- domainPath: getConceptDomainPath(state)
41
+ const mapStateToProps = (state) => ({
42
+ domainPath: getConceptDomainPath(state),
35
43
  });
36
44
 
37
45
  export default connect(mapStateToProps)(ConceptTaxonomy);
@@ -0,0 +1,43 @@
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 { ConceptManageDomain } from "../ConceptManageDomain";
6
+
7
+ describe("<ConceptManageDomain />", () => {
8
+ // const id = 5;
9
+ const domainOptions = [
10
+ { id: 1, name: "foo", level: 0 },
11
+ { id: 2, name: "bar", level: 0 },
12
+ { id: 3, name: "baz", level: 0 },
13
+ ];
14
+ const conceptAction = jest.fn();
15
+ // const saving = false;
16
+ const setToDomain = [{ id: 2, name: "bar" }];
17
+ const props = {
18
+ domainOptions,
19
+ // id,
20
+ conceptAction,
21
+ // saving,
22
+ setToDomain,
23
+ };
24
+ const renderOpts = {
25
+ messages: {
26
+ en: {
27
+ "concept.changeDomain.header": "edit domain",
28
+ "concept.changeDomain.label": "domain",
29
+ "conceptDomain.actions.update": "update",
30
+ "domain.selector.label": "domain",
31
+ "domain.selector.placeholder": "select a domain...",
32
+ },
33
+ },
34
+ };
35
+
36
+ it("matches the latest snapshot", () => {
37
+ const { container } = render(
38
+ <ConceptManageDomain {...props} />,
39
+ renderOpts
40
+ );
41
+ expect(container).toMatchSnapshot();
42
+ });
43
+ });
@@ -0,0 +1,36 @@
1
+ import React from "react";
2
+ import { mount } from "enzyme";
3
+ import { intl } from "@truedat/test/intl-stub";
4
+ import { ConceptManageDomainPopup } from "../ConceptManageDomainPopup";
5
+
6
+ // workaround for enzyme issue with React.useContext
7
+ // see https://github.com/airbnb/enzyme/issues/2176#issuecomment-532361526
8
+ jest.spyOn(React, "useContext").mockImplementation(() => intl);
9
+ jest.mock("../ConceptManageDomain");
10
+
11
+ describe("<ConceptManageDomainPopup />", () => {
12
+ const props = {
13
+ saving: false,
14
+ update_domain: true,
15
+ };
16
+
17
+ it("matches the latest snapshot", () => {
18
+ const wrapper = mount(<ConceptManageDomainPopup {...props} />);
19
+ expect(wrapper).toMatchSnapshot();
20
+ });
21
+
22
+ it("manages open and close popup", () => {
23
+ const wrapper = mount(<ConceptManageDomainPopup {...props} />);
24
+ wrapper.setProps({ saving: true, update_domain: true });
25
+ expect(wrapper.find("Popup").prop("open")).toBeFalsy();
26
+ wrapper.find("Popup").find("Portal").find("Button").simulate("click");
27
+ expect(wrapper.find("Popup").prop("open")).toBeTruthy();
28
+ wrapper.find("Popup").find("Portal").find("Button").simulate("click");
29
+ expect(wrapper.find("Popup").prop("open")).toBeFalsy();
30
+ wrapper.find("Popup").find("Portal").find("Button").simulate("click");
31
+ expect(wrapper.find("Popup").prop("open")).toBeTruthy();
32
+ wrapper.setProps({ saving: false, update_domain: true });
33
+ wrapper.update();
34
+ expect(wrapper.find("Popup").prop("open")).toBeFalsy();
35
+ });
36
+ });
@@ -0,0 +1,129 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`<ConceptManageDomain /> 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
+ edit domain
13
+ </h2>
14
+ <form
15
+ class="ui form"
16
+ >
17
+ <div
18
+ class="required field"
19
+ >
20
+ <label>
21
+ domain
22
+ </label>
23
+ <div
24
+ class="field"
25
+ >
26
+ <div
27
+ aria-expanded="false"
28
+ class="ui fluid search selection dropdown"
29
+ name="domain"
30
+ role="combobox"
31
+ >
32
+ <input
33
+ aria-autocomplete="list"
34
+ autocomplete="off"
35
+ class="search"
36
+ tabindex="0"
37
+ type="text"
38
+ value=""
39
+ />
40
+ <div
41
+ aria-atomic="true"
42
+ aria-live="polite"
43
+ class="divider default text"
44
+ role="alert"
45
+ >
46
+ select a domain...
47
+ </div>
48
+ <i
49
+ aria-hidden="true"
50
+ class="dropdown icon"
51
+ />
52
+ <div
53
+ class="menu transition"
54
+ role="listbox"
55
+ >
56
+ <div
57
+ aria-checked="false"
58
+ aria-selected="true"
59
+ class="selected item"
60
+ role="option"
61
+ style="pointer-events: all;"
62
+ >
63
+ <div
64
+ class="text"
65
+ style="margin-left: 0px;"
66
+ >
67
+ <i
68
+ aria-hidden="true"
69
+ class="icon"
70
+ />
71
+ foo
72
+ </div>
73
+ </div>
74
+ <div
75
+ aria-checked="false"
76
+ aria-selected="false"
77
+ class="item"
78
+ role="option"
79
+ style="pointer-events: all;"
80
+ >
81
+ <div
82
+ class="text"
83
+ style="margin-left: 0px;"
84
+ >
85
+ <i
86
+ aria-hidden="true"
87
+ class="icon"
88
+ />
89
+ bar
90
+ </div>
91
+ </div>
92
+ <div
93
+ aria-checked="false"
94
+ aria-selected="false"
95
+ class="item"
96
+ role="option"
97
+ style="pointer-events: all;"
98
+ >
99
+ <div
100
+ class="text"
101
+ style="margin-left: 0px;"
102
+ >
103
+ <i
104
+ aria-hidden="true"
105
+ class="icon"
106
+ />
107
+ baz
108
+ </div>
109
+ </div>
110
+ </div>
111
+ </div>
112
+ </div>
113
+ </div>
114
+ <div
115
+ class="actions"
116
+ >
117
+ <button
118
+ class="ui primary disabled button"
119
+ disabled=""
120
+ tabindex="-1"
121
+ type="submit"
122
+ >
123
+ update
124
+ </button>
125
+ </div>
126
+ </form>
127
+ </div>
128
+ </div>
129
+ `;
@@ -0,0 +1,171 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`<ConceptManageDomainPopup /> matches the latest snapshot 1`] = `
4
+ <ConceptManageDomainPopup
5
+ saving={false}
6
+ update_domain={true}
7
+ >
8
+ <Popup
9
+ basic={true}
10
+ content={<Memo(Connect(ConceptManageDomain)) />}
11
+ disabled={false}
12
+ eventsEnabled={true}
13
+ flowing={true}
14
+ on="click"
15
+ onClose={[Function]}
16
+ onOpen={[Function]}
17
+ open={false}
18
+ pinned={false}
19
+ popperModifiers={Array []}
20
+ position="bottom right"
21
+ positionFixed={true}
22
+ size="large"
23
+ trigger={
24
+ <Button
25
+ as="button"
26
+ basic={true}
27
+ icon={
28
+ <Icon
29
+ as="i"
30
+ name="pencil alternate"
31
+ />
32
+ }
33
+ style={
34
+ Object {
35
+ "boxShadow": "none",
36
+ }
37
+ }
38
+ />
39
+ }
40
+ >
41
+ <Portal
42
+ closeOnDocumentClick={true}
43
+ closeOnEscape={true}
44
+ closeOnTriggerClick={true}
45
+ eventPool="default"
46
+ onClose={[Function]}
47
+ onMount={[Function]}
48
+ onOpen={[Function]}
49
+ onUnmount={[Function]}
50
+ open={false}
51
+ openOnTriggerClick={true}
52
+ trigger={
53
+ <Button
54
+ as="button"
55
+ basic={true}
56
+ icon={
57
+ <Icon
58
+ as="i"
59
+ name="pencil alternate"
60
+ />
61
+ }
62
+ style={
63
+ Object {
64
+ "boxShadow": "none",
65
+ }
66
+ }
67
+ />
68
+ }
69
+ triggerRef={
70
+ Object {
71
+ "current": <button
72
+ class="ui basic icon button"
73
+ style="box-shadow: none;"
74
+ >
75
+ <i
76
+ aria-hidden="true"
77
+ class="pencil alternate icon"
78
+ />
79
+ </button>,
80
+ }
81
+ }
82
+ >
83
+ <Ref
84
+ innerRef={[Function]}
85
+ >
86
+ <RefFindNode
87
+ innerRef={[Function]}
88
+ >
89
+ <Button
90
+ as="button"
91
+ basic={true}
92
+ icon={
93
+ <Icon
94
+ as="i"
95
+ name="pencil alternate"
96
+ />
97
+ }
98
+ onBlur={[Function]}
99
+ onClick={[Function]}
100
+ onFocus={[Function]}
101
+ onMouseEnter={[Function]}
102
+ onMouseLeave={[Function]}
103
+ style={
104
+ Object {
105
+ "boxShadow": "none",
106
+ }
107
+ }
108
+ >
109
+ <Ref
110
+ innerRef={
111
+ Object {
112
+ "current": <button
113
+ class="ui basic icon button"
114
+ style="box-shadow: none;"
115
+ >
116
+ <i
117
+ aria-hidden="true"
118
+ class="pencil alternate icon"
119
+ />
120
+ </button>,
121
+ }
122
+ }
123
+ >
124
+ <RefFindNode
125
+ innerRef={
126
+ Object {
127
+ "current": <button
128
+ class="ui basic icon button"
129
+ style="box-shadow: none;"
130
+ >
131
+ <i
132
+ aria-hidden="true"
133
+ class="pencil alternate icon"
134
+ />
135
+ </button>,
136
+ }
137
+ }
138
+ >
139
+ <button
140
+ className="ui basic icon button"
141
+ onBlur={[Function]}
142
+ onClick={[Function]}
143
+ onFocus={[Function]}
144
+ onMouseEnter={[Function]}
145
+ onMouseLeave={[Function]}
146
+ style={
147
+ Object {
148
+ "boxShadow": "none",
149
+ }
150
+ }
151
+ >
152
+ <Icon
153
+ as="i"
154
+ name="pencil alternate"
155
+ >
156
+ <i
157
+ aria-hidden="true"
158
+ className="pencil alternate icon"
159
+ onClick={[Function]}
160
+ />
161
+ </Icon>
162
+ </button>
163
+ </RefFindNode>
164
+ </Ref>
165
+ </Button>
166
+ </RefFindNode>
167
+ </Ref>
168
+ </Portal>
169
+ </Popup>
170
+ </ConceptManageDomainPopup>
171
+ `;
@@ -11,6 +11,12 @@ exports[`<ConceptTaxonomy /> matches the latest snapshot 1`] = `
11
11
  id="concepts.taxonomy"
12
12
  />
13
13
  </Header>
14
+ <lazy
15
+ actions="manage_business_concepts_domain"
16
+ />
17
+ <Connect(ConceptManageDomainPopup)
18
+ conceptAction="concepts.actions.edit"
19
+ />
14
20
  <List
15
21
  horizontal={true}
16
22
  >
@@ -28,6 +28,7 @@ import { conceptUserFilters } from "./conceptUserFilters";
28
28
  import { conceptSelectedUserFilter } from "./conceptSelectedUserFilter";
29
29
  import { sharedToDomains } from "./sharedToDomains";
30
30
  import { savingSharedTo } from "./savingSharedTo";
31
+ import { updatingDomain } from "./updatingDomain";
31
32
 
32
33
  export {
33
34
  bulkUpdateLoading,
@@ -59,7 +60,8 @@ export {
59
60
  conceptUpdating,
60
61
  conceptUserFilters,
61
62
  sharedToDomains,
62
- savingSharedTo
63
+ savingSharedTo,
64
+ updatingDomain,
63
65
  };
64
66
 
65
67
  export * from "../relations/reducers";
@@ -0,0 +1,16 @@
1
+ import { conceptAction } from "../routines";
2
+
3
+ const initialState = false;
4
+
5
+ export const updatingDomain = (state = initialState, { type }) => {
6
+ switch (type) {
7
+ case conceptAction.TRIGGER:
8
+ return true;
9
+ case conceptAction.FULFILL:
10
+ return false;
11
+ default:
12
+ return state;
13
+ }
14
+ };
15
+
16
+ export default updatingDomain;
@@ -13,6 +13,8 @@ export default {
13
13
  "Repeated Concept name",
14
14
  "bulkUpdate.no.concepts.body": "No concepts selected",
15
15
  "bulkUpdate.no.concepts.header": "Empty Search",
16
+ business_concept_to_field_master: "Master Relation",
17
+ "conceptDomain.actions.update": "Update",
16
18
  "conceptRelations.relationType.business_concept_to_field_master":
17
19
  "Master Relation",
18
20
  "concept.error.existing.business.concept":
@@ -54,6 +56,8 @@ export default {
54
56
  "concept.sharedTo.dropdown.label": "Domain",
55
57
  "concept.sharedTo.dropdown.placeholder": "Select domain",
56
58
  "concept.sharedTo.header": "Share in",
59
+ "concept.changeDomain.header": "Edit Domain",
60
+ "concept.changeDomain.label": "Domain",
57
61
  "conceptRelation.actions.create": "New Link",
58
62
  "conceptRelation.actions.delete.confirmation.content":
59
63
  "The Concept will be unlinked from the field. Are you sure?",
@@ -15,6 +15,8 @@ export default {
15
15
  "Nombre de concepto repetido",
16
16
  "bulkUpdate.no.concepts.body": "Ningún concepto seleccionado",
17
17
  "bulkUpdate.no.concepts.header": "Búsqueda vacía",
18
+ business_concept_to_field_master: "Relación master",
19
+ "conceptDomain.actions.update": "Actualizar",
18
20
  "conceptRelations.relationType.business_concept_to_field_master":
19
21
  "Relación master",
20
22
  "concept.error.existing.business.concept":
@@ -56,6 +58,8 @@ export default {
56
58
  "concept.sharedTo.dropdown.label": "Dominios",
57
59
  "concept.sharedTo.dropdown.placeholder": "Seleccionar dominio",
58
60
  "concept.sharedTo.header": "Compartir en",
61
+ "concept.changeDomain.label": "Dominios",
62
+ "concept.changeDomain.header": "Editar Dominio",
59
63
  "conceptRelation.actions.create": "Crear vínculo",
60
64
  "conceptRelation.actions.delete.confirmation.content":
61
65
  "Se eliminará esta vinculación. ¿Estás seguro?",
@@ -0,0 +1,43 @@
1
+ import React from "react";
2
+ import PropTypes from "prop-types";
3
+ import { connect } from "react-redux";
4
+ import { Loading } from "@truedat/core/components";
5
+ import { clearDomainsConcept, fetchDomainsConcept } from "../routines";
6
+
7
+ export class DomainsConceptLoader extends React.Component {
8
+ static propTypes = {
9
+ actions: PropTypes.string,
10
+ clearDomainsConcept: PropTypes.func,
11
+ fetchDomainsConcept: PropTypes.func,
12
+ filter: PropTypes.string,
13
+ domainsConceptLoading: PropTypes.bool,
14
+ };
15
+
16
+ componentDidMount() {
17
+ const { fetchDomainsConcept, actions, filter } = this.props;
18
+ fetchDomainsConcept({ actions, filter });
19
+ }
20
+
21
+ componentWillUnmount() {
22
+ const { clearDomainsConcept } = this.props;
23
+ clearDomainsConcept();
24
+ }
25
+
26
+ render() {
27
+ const { domainsConceptLoading } = this.props;
28
+ if (domainsConceptLoading) {
29
+ return <Loading />;
30
+ } else {
31
+ return null;
32
+ }
33
+ }
34
+ }
35
+
36
+ const mapStateToProps = ({ domainsConceptLoading }) => ({
37
+ domainsConceptLoading,
38
+ });
39
+
40
+ export default connect(mapStateToProps, {
41
+ clearDomainsConcept,
42
+ fetchDomainsConcept,
43
+ })(DomainsConceptLoader);
@@ -0,0 +1,38 @@
1
+ import React from "react";
2
+ import { mount } from "enzyme";
3
+ import { DomainsConceptLoader } from "../DomainsConceptLoader";
4
+
5
+ const getProps = () => ({
6
+ clearDomainsConcept: jest.fn(),
7
+ domainsConceptLoading: false,
8
+ fetchDomainsConcept: jest.fn(),
9
+ actions: "create_ingest",
10
+ filter: "all",
11
+ });
12
+
13
+ describe("<DomainsConceptLoader />", () => {
14
+ it("calls fetchDomainsConcept when component mounts but not when it unmounts", () => {
15
+ const props = getProps();
16
+ const wrapper = mount(<DomainsConceptLoader {...props} />);
17
+ expect(props.fetchDomainsConcept).toHaveBeenCalledTimes(1);
18
+ wrapper.unmount();
19
+ expect(props.fetchDomainsConcept).toHaveBeenCalledTimes(1);
20
+ });
21
+
22
+ it("calls clearDomainsConcept and clearDomainDefaultFilters when component unmounts but not when it mounts", () => {
23
+ const props = getProps();
24
+ const wrapper = mount(<DomainsConceptLoader {...props} />);
25
+ expect(props.clearDomainsConcept).toHaveBeenCalledTimes(0);
26
+ wrapper.unmount();
27
+ expect(props.clearDomainsConcept).toHaveBeenCalledTimes(1);
28
+ });
29
+
30
+ it("renders Loading when domainLoading is true", () => {
31
+ const props = getProps();
32
+
33
+ const wrapper = mount(
34
+ <DomainsConceptLoader {...{ ...props, domainsConceptLoading: true }} />
35
+ );
36
+ expect(wrapper.find("Loading").length).toBe(1);
37
+ });
38
+ });
@@ -0,0 +1,26 @@
1
+ import { fetchDomainsConcept } from "../../routines";
2
+ import { domainsConceptLoading } from "..";
3
+
4
+ const fooState = { foo: "bar" };
5
+
6
+ describe("reducers: domainsConceptLoading", () => {
7
+ it("should provide the initial state", () => {
8
+ expect(domainsConceptLoading(undefined, {})).toBe(false);
9
+ });
10
+
11
+ it("should be true after receiving the fetchDomainsConcept.TRIGGER action", () => {
12
+ expect(
13
+ domainsConceptLoading(false, { type: fetchDomainsConcept.TRIGGER })
14
+ ).toBe(true);
15
+ });
16
+
17
+ it("should be false after receiving the fetchDomainsConcept.FULFILL action", () => {
18
+ expect(
19
+ domainsConceptLoading(true, { type: fetchDomainsConcept.FULFILL })
20
+ ).toBe(false);
21
+ });
22
+
23
+ it("should ignore unhandled actions", () => {
24
+ expect(domainsConceptLoading(fooState, { type: "FOO" })).toBe(fooState);
25
+ });
26
+ });
@@ -0,0 +1,30 @@
1
+ import { fetchDomainsConcept } from "../../routines";
2
+ import { domainsConcept } from "..";
3
+
4
+ const fooState = { foo: "bar" };
5
+
6
+ describe("reducers: domainsConcept", () => {
7
+ const initialState = [];
8
+
9
+ it("should provide the initial state", () => {
10
+ expect(domainsConcept(undefined, {})).toEqual(initialState);
11
+ });
12
+
13
+ it("should handle the SUCCESS action", () => {
14
+ const data = [
15
+ { id: 1, name: "Domain 1", description: "Desc 1" },
16
+ { id: 2, name: "Domain 2", description: "Desc 2", parent_id: 1 },
17
+ ];
18
+
19
+ expect(
20
+ domainsConcept(fooState, {
21
+ type: fetchDomainsConcept.SUCCESS,
22
+ payload: { data: { data } },
23
+ })
24
+ ).toMatchObject(data);
25
+ });
26
+
27
+ it("should ignore unknown actions", () => {
28
+ expect(domainsConcept(fooState, { type: "FOO" })).toBe(fooState);
29
+ });
30
+ });
@@ -0,0 +1,19 @@
1
+ import _ from "lodash/fp";
2
+ import { fetchDomainsConcept, clearDomainsConcept } from "../routines";
3
+
4
+ const initialState = [];
5
+
6
+ const caseInsensitiveName = ({ name }) => _.toLower(name);
7
+
8
+ export const domainsConcept = (state = initialState, { type, payload }) => {
9
+ switch (type) {
10
+ case fetchDomainsConcept.SUCCESS:
11
+ const { data } = payload;
12
+ const collection = _.prop("data")(data);
13
+ return _.sortBy(caseInsensitiveName)(collection);
14
+ case clearDomainsConcept.TRIGGER:
15
+ return initialState;
16
+ default:
17
+ return state;
18
+ }
19
+ };
@@ -0,0 +1,12 @@
1
+ import { fetchDomainsConcept } from "../routines";
2
+
3
+ export const domainsConceptLoading = (state = false, { type }) => {
4
+ switch (type) {
5
+ case fetchDomainsConcept.TRIGGER:
6
+ return true;
7
+ case fetchDomainsConcept.FULFILL:
8
+ return false;
9
+ default:
10
+ return state;
11
+ }
12
+ };
@@ -5,6 +5,8 @@ import { domainLoading } from "./domainLoading";
5
5
  import { domainMemberDeleting } from "./domainMemberDeleting";
6
6
  import { domainMemberSaving } from "./domainMemberSaving";
7
7
  import { domainMembers } from "./domainMembers";
8
+ import { domainsConcept } from "./domainsConcept";
9
+ import { domainsConceptLoading } from "./domainsConceptLoading";
8
10
  import { domainConceptChildren } from "./domainConceptChildren";
9
11
  import { domainConceptChildrenLoading } from "./domainConceptChildrenLoading";
10
12
  import { domainMembersActions } from "./domainMembersActions";
@@ -26,6 +28,8 @@ export {
26
28
  domainMembersActions,
27
29
  domainMembers,
28
30
  domainMembersLoading,
31
+ domainsConcept,
32
+ domainsConceptLoading,
29
33
  domainConceptChildren,
30
34
  domainConceptChildrenLoading,
31
35
  domainRedirect,
@@ -1,6 +1,8 @@
1
1
  import { createRoutine } from "redux-saga-routines";
2
2
 
3
3
  export const fetchDomains = createRoutine("FETCH_DOMAINS");
4
+ export const fetchDomainsConcept = createRoutine("FETCH_CONCEPT_DOMAINS");
5
+ export const clearDomainsConcept = createRoutine("CLEAR_DOMAINS");
4
6
  export const clearDomains = createRoutine("CLEAR_DOMAINS");
5
7
  export const filterDomains = createRoutine("FILTER_DOMAINS");
6
8
  export const clearDomainConceptChildren = createRoutine(
@@ -0,0 +1,93 @@
1
+ import { testSaga } from "redux-saga-test-plan";
2
+ import { apiJson, JSON_OPTS } from "@truedat/core/services/api";
3
+ import {
4
+ fetchDomainsConceptRequestSaga,
5
+ fetchDomainsConceptSaga,
6
+ } from "../fetchDomainsConcept";
7
+ import { fetchDomainsConcept } from "../../routines";
8
+ import { API_DOMAINS } from "../../api";
9
+
10
+ describe("sagas: fetchDomainsConceptRequestSaga", () => {
11
+ it("should invoke fetchDomainsConceptSaga on trigger", () => {
12
+ expect(() => {
13
+ testSaga(fetchDomainsConceptRequestSaga)
14
+ .next()
15
+ .takeLatest(fetchDomainsConcept.TRIGGER, fetchDomainsConceptSaga)
16
+ .finish()
17
+ .isDone();
18
+ }).not.toThrow();
19
+ });
20
+
21
+ it("should throw exception if an unhandled action is received", () => {
22
+ expect(() => {
23
+ testSaga(fetchDomainsConceptRequestSaga)
24
+ .next()
25
+ .takeLatest("FOO", fetchDomainsConceptSaga);
26
+ }).toThrow();
27
+ });
28
+ });
29
+
30
+ describe("sagas: fetchDomainsConceptSaga", () => {
31
+ const data = {
32
+ collection: [
33
+ { id: 1, name: "Top domain", description: "Desc" },
34
+ { id: 2, name: "Child domain", description: "Desc2", parent_id: 2 },
35
+ ],
36
+ };
37
+
38
+ it("should put a success action when a response is returned", () => {
39
+ const actions = undefined;
40
+ expect(() => {
41
+ testSaga(fetchDomainsConceptSaga, {})
42
+ .next()
43
+ .put(fetchDomainsConcept.request())
44
+ .next()
45
+ .call(apiJson, API_DOMAINS, JSON_OPTS)
46
+ .next({ data })
47
+ .put(fetchDomainsConcept.success({ data, actions }))
48
+ .next()
49
+ .put(fetchDomainsConcept.fulfill())
50
+ .next()
51
+ .isDone();
52
+ }).not.toThrow();
53
+ });
54
+
55
+ it("should handle actions in the payload", () => {
56
+ const actions = "show";
57
+ const filter = "foo";
58
+ const payload = { actions, filter };
59
+ const json_opts = { params: { actions, filter }, ...JSON_OPTS };
60
+ expect(() => {
61
+ testSaga(fetchDomainsConceptSaga, { payload })
62
+ .next()
63
+ .put(fetchDomainsConcept.request(payload))
64
+ .next()
65
+ .call(apiJson, API_DOMAINS, json_opts)
66
+ .next({ data })
67
+ .put(fetchDomainsConcept.success({ data, actions }))
68
+ .next()
69
+ .put(fetchDomainsConcept.fulfill())
70
+ .next()
71
+ .isDone();
72
+ }).not.toThrow();
73
+ });
74
+
75
+ it("should put a failure action when the call throws an error", () => {
76
+ const message = "Request failed";
77
+ const error = { message };
78
+
79
+ expect(() => {
80
+ testSaga(fetchDomainsConceptSaga, {})
81
+ .next()
82
+ .put(fetchDomainsConcept.request())
83
+ .next()
84
+ .call(apiJson, API_DOMAINS, JSON_OPTS)
85
+ .throw(error)
86
+ .put(fetchDomainsConcept.failure(message))
87
+ .next()
88
+ .put(fetchDomainsConcept.fulfill())
89
+ .next()
90
+ .isDone();
91
+ }).not.toThrow();
92
+ });
93
+ });
@@ -0,0 +1,37 @@
1
+ import _ from "lodash/fp";
2
+ import { call, put, takeLatest } from "redux-saga/effects";
3
+ import { apiJson, JSON_OPTS } from "@truedat/core/services/api";
4
+ import { fetchDomainsConcept } from "../routines";
5
+ import { API_DOMAINS } from "../api";
6
+
7
+ export function* fetchDomainsConceptSaga({ payload }) {
8
+ try {
9
+ const { actions, filter } = payload || {};
10
+ const json_opts = actions
11
+ ? {
12
+ ...JSON_OPTS,
13
+ params: {
14
+ actions: _.flow(_.castArray, _.join(","))(actions),
15
+ filter,
16
+ },
17
+ }
18
+ : JSON_OPTS;
19
+ const url = API_DOMAINS;
20
+ yield put(fetchDomainsConcept.request(payload));
21
+ const { data } = yield call(apiJson, url, json_opts);
22
+ yield put(fetchDomainsConcept.success({ data, actions }));
23
+ } catch (error) {
24
+ if (error.response) {
25
+ const { status, data } = error.response;
26
+ yield put(fetchDomainsConcept.failure({ status, data }));
27
+ } else {
28
+ yield put(fetchDomainsConcept.failure(error.message));
29
+ }
30
+ } finally {
31
+ yield put(fetchDomainsConcept.fulfill());
32
+ }
33
+ }
34
+
35
+ export function* fetchDomainsConceptRequestSaga() {
36
+ yield takeLatest(fetchDomainsConcept.TRIGGER, fetchDomainsConceptSaga);
37
+ }
@@ -4,6 +4,7 @@ import { deleteDomainMemberRequestSaga } from "./deleteDomainMember";
4
4
  import { updateDomainMemberRequestSaga } from "./updateDomainMember";
5
5
  import { updateDomainRequestSaga } from "./updateDomain";
6
6
  import { deleteDomainRequestSaga } from "./deleteDomain";
7
+ import { fetchDomainsConceptRequestSaga } from "./fetchDomainsConcept";
7
8
  import { fetchDomainConceptChildrenRequestSaga } from "./fetchDomainConceptChildren";
8
9
  import { fetchDomainMembersRequestSaga } from "./fetchDomainMembers";
9
10
  import { fetchDomainRequestSaga } from "./fetchDomain";
@@ -16,6 +17,7 @@ export {
16
17
  updateDomainMemberRequestSaga,
17
18
  updateDomainRequestSaga,
18
19
  deleteDomainRequestSaga,
20
+ fetchDomainsConceptRequestSaga,
19
21
  fetchDomainConceptChildrenRequestSaga,
20
22
  fetchDomainMembersRequestSaga,
21
23
  fetchDomainRequestSaga,
@@ -29,6 +31,7 @@ export default [
29
31
  updateDomainMemberRequestSaga(),
30
32
  updateDomainRequestSaga(),
31
33
  deleteDomainRequestSaga(),
34
+ fetchDomainsConceptRequestSaga(),
32
35
  fetchDomainConceptChildrenRequestSaga(),
33
36
  fetchDomainMembersRequestSaga(),
34
37
  fetchDomainRequestSaga(),
@@ -0,0 +1,176 @@
1
+ import _ from "lodash/fp";
2
+ import { createSelector } from "reselect";
3
+ import { accentInsensitivePathOrder } from "@truedat/core/services/sort";
4
+ import { getPrimaryActions, getSecondaryActions } from "./getDomainActions";
5
+ import { getDomainGroups } from "./getDomainGroups";
6
+ import { domainParentOptionsSelector } from "./domainParentOptionsSelector";
7
+
8
+ const getDomain = ({ domain }) => domain;
9
+ const getDomains = ({ domainsConcept }) => domainsConcept;
10
+ const getDomainsFilter = ({ domainsFilter }) => domainsFilter;
11
+
12
+ const isChildOf =
13
+ ({ id }) =>
14
+ ({ parent_id }) =>
15
+ parent_id === id;
16
+ const isParentOf =
17
+ ({ parent_id }) =>
18
+ ({ id }) =>
19
+ parent_id === id;
20
+
21
+ const hasNoParentIn = (domainsConcept) => (domain) =>
22
+ !_.some(isParentOf(domain))(domainsConcept);
23
+
24
+ /**
25
+ * Creates a selector which returns the child domainsConcept of the currently selected domain.
26
+ * If no domain is currently selected, it returns the domainsConcept for which no parent is found.
27
+ */
28
+ const getChildOrRootDomains = createSelector(
29
+ [getDomain, getDomains],
30
+ (domain, domainsConcept) => {
31
+ return _.isEmpty(domain)
32
+ ? _.filter(hasNoParentIn(domainsConcept))(domainsConcept)
33
+ : _.filter(isChildOf(domain))(domainsConcept);
34
+ }
35
+ );
36
+
37
+ const findParentsIn = (domainsConcept) => (child) =>
38
+ _.filter(isParentOf(child))(domainsConcept);
39
+ const findChildrenIn = (domainsConcept) => (parent) =>
40
+ _.filter(isChildOf(parent))(domainsConcept);
41
+
42
+ const findDescendents = (parents) => (domainsConcept) => {
43
+ const children = _.flatMap(findChildrenIn(domainsConcept))(parents);
44
+ return _.isEmpty(children)
45
+ ? children
46
+ : _.concat(children, findDescendents(children)(domainsConcept));
47
+ };
48
+
49
+ /**
50
+ * A selector to compute the descendents of the currently selected domain.
51
+ * If no domain is currently selected, all domainsConcept are returned.
52
+ */
53
+ const getDescendents = createSelector(
54
+ [getDomain, getDomains],
55
+ (domain, domainsConcept) =>
56
+ _.isEmpty(domain)
57
+ ? domainsConcept
58
+ : findDescendents([domain])(domainsConcept)
59
+ );
60
+
61
+ const toSearchable = _.flow(_.deburr, _.toLower, _.trim);
62
+
63
+ const isValidFilter = (filter) => !_.isEmpty(toSearchable(filter));
64
+
65
+ const matchesFilter = (filter) =>
66
+ _.flow(
67
+ _.at(["name", "description"]),
68
+ _.map(toSearchable),
69
+ _.some(_.includes(toSearchable(filter)))
70
+ );
71
+
72
+ /**
73
+ * A selector to compute the domainsConcept whose name or description matches a search string.
74
+ * If a domain is currently selected, the scope of the search will be limited to descendents
75
+ * of the currently selected domain. The search is case-insensitive and accent-insensitive.
76
+ */
77
+ const getFilteredDomains = createSelector(
78
+ [getDescendents, getDomainsFilter],
79
+ (descendents, domainsFilter) =>
80
+ isValidFilter(domainsFilter)
81
+ ? _.filter(matchesFilter(domainsFilter))(descendents)
82
+ : []
83
+ );
84
+
85
+ const _getVisibleDomains = createSelector(
86
+ [getDomainsFilter, getFilteredDomains, getChildOrRootDomains],
87
+ (filter, filteredDomains, childOrRootDomains) =>
88
+ _.isEmpty(filter) ? childOrRootDomains : filteredDomains
89
+ );
90
+
91
+ /**
92
+ * A selector to compute the currently visible domainsConcept. If a filter is present, the selector
93
+ * returns descendent domainsConcept matching the filter within the scope of the currently selected
94
+ * domain (or within the scope of all domainsConcept if no domain is currently selected).
95
+ * If no filter is present, the selector returns domainsConcept without parents.
96
+ * Enriches domainsConcept with child count.
97
+ */
98
+ const getVisibleDomains = createSelector(
99
+ [_getVisibleDomains, getDomains],
100
+ (visibleDomains, domainsConcept) =>
101
+ visibleDomains
102
+ .map((d) => ({
103
+ ...d,
104
+ children: _.filter(isChildOf(d))(domainsConcept),
105
+ }))
106
+ .map(({ children, ...d }) => ({ childCount: children.length, ...d }))
107
+ );
108
+
109
+ /**
110
+ * A selector to obtain unique domain types, sorted case-insensitively.
111
+ */
112
+ const getDomainTypes = createSelector(
113
+ getDomains,
114
+ _.flow(_.map("type"), _.filter(_.isString), _.uniq, _.sortBy(_.toLower))
115
+ );
116
+
117
+ const findAncestorsIn = (domainsConcept) => (children) => {
118
+ const parents = _.flatMap(findParentsIn(domainsConcept))(children);
119
+ return _.isEmpty(parents)
120
+ ? parents
121
+ : _.concat(findAncestorsIn(domainsConcept)(parents), parents);
122
+ };
123
+
124
+ /**
125
+ * A selector to obtain the parents of the currently selected domain.
126
+ */
127
+ const getAncestorDomains = createSelector(
128
+ [getDomains, getDomain],
129
+ (domainsConcept, domain) =>
130
+ _.isEmpty(domain) ? [] : findAncestorsIn(domainsConcept)([domain])
131
+ );
132
+
133
+ const reduceTaxonomy = (domainsConcept) => (domainsInLevel, level) => {
134
+ if (domainsConcept)
135
+ return _.reduce((acc, domain) => {
136
+ const children = findChildrenIn(domainsConcept)(domain);
137
+ return [
138
+ ...acc,
139
+ {
140
+ ...domain,
141
+ ancestors: findAncestorsIn(domainsConcept)([domain]),
142
+ descendents: findDescendents([domain])(domainsConcept),
143
+ children,
144
+ level,
145
+ },
146
+ ...reduceTaxonomy(domainsConcept)(children, level + 1),
147
+ ];
148
+ }, [])(domainsInLevel);
149
+ return [];
150
+ };
151
+
152
+ const getDomainConceptSelectorOptions = createSelector(
153
+ [getDomains],
154
+ (domainsConcept) => {
155
+ const roots = getChildOrRootDomains({
156
+ domainsConcept: _.sortBy(accentInsensitivePathOrder("name"))(
157
+ domainsConcept
158
+ ),
159
+ });
160
+ return reduceTaxonomy(domainsConcept)(roots, 0);
161
+ }
162
+ );
163
+
164
+ export {
165
+ domainParentOptionsSelector,
166
+ getChildOrRootDomains,
167
+ getDescendents,
168
+ getDomainConceptSelectorOptions,
169
+ getFilteredDomains,
170
+ getVisibleDomains,
171
+ getDomainGroups,
172
+ getDomainTypes,
173
+ getAncestorDomains,
174
+ getPrimaryActions,
175
+ getSecondaryActions,
176
+ };