@truedat/df 4.44.2 → 4.44.5

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [4.44.5] 2022-05-20
4
+
5
+ ### Added
6
+
7
+ - [TD-4230] Created `SelectableDynamicForm` that wraps `TemplateSelector` and `DynamicForm` with common business logic
8
+
9
+ ### Changed
10
+
11
+ - [TD-4230] `TemplateLoader` works with multiple domain ids
12
+
3
13
  ## [4.38.4] 2022-02-20
4
14
 
5
15
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/df",
3
- "version": "4.44.2",
3
+ "version": "4.44.5",
4
4
  "description": "Truedat Web Data Quality Module",
5
5
  "sideEffects": false,
6
6
  "jsnext:main": "src/index.js",
@@ -16,22 +16,26 @@
16
16
  "scripts": {
17
17
  "clean": "rimraf yarn-error.log",
18
18
  "debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
19
- "test": "jest --coverage",
20
- "test:watch": "jest --watch",
19
+ "test": "TZ=UTC jest --coverage",
20
+ "test:watch": "TZ=UTC jest --watch",
21
21
  "eslint": "eslint src/**",
22
22
  "eslint:fix": "eslint --fix src/**"
23
23
  },
24
24
  "devDependencies": {
25
- "@babel/cli": "^7.14.8",
26
- "@babel/core": "^7.15.0",
27
- "@babel/plugin-proposal-class-properties": "^7.14.5",
28
- "@babel/plugin-proposal-object-rest-spread": "^7.14.7",
25
+ "@babel/cli": "^7.17.10",
26
+ "@babel/core": "^7.18.0",
27
+ "@babel/plugin-proposal-class-properties": "^7.17.12",
28
+ "@babel/plugin-proposal-object-rest-spread": "^7.18.0",
29
+ "@babel/plugin-proposal-optional-chaining": "^7.17.12",
29
30
  "@babel/plugin-syntax-dynamic-import": "^7.8.3",
30
- "@babel/plugin-transform-modules-commonjs": "^7.15.0",
31
- "@babel/preset-env": "^7.15.0",
32
- "@babel/preset-react": "^7.14.5",
33
- "@truedat/test": "4.44.2",
34
- "babel-jest": "^27.0.6",
31
+ "@babel/plugin-transform-modules-commonjs": "^7.18.0",
32
+ "@babel/preset-env": "^7.18.0",
33
+ "@babel/preset-react": "^7.17.12",
34
+ "@testing-library/jest-dom": "^5.16.4",
35
+ "@testing-library/react": "^12.0.0",
36
+ "@testing-library/user-event": "^13.2.1",
37
+ "@truedat/test": "4.44.5",
38
+ "babel-jest": "^28.1.0",
35
39
  "babel-plugin-dynamic-import-node": "^2.3.3",
36
40
  "babel-plugin-lodash": "^3.3.4",
37
41
  "babel-plugin-react-intl": "^5.1.18",
@@ -40,7 +44,8 @@
40
44
  "enzyme-adapter-react-16": "^1.15.6",
41
45
  "enzyme-to-json": "^3.6.2",
42
46
  "identity-obj-proxy": "^3.0.0",
43
- "jest": "^27.0.6",
47
+ "jest": "^28.1.0",
48
+ "jest-environment-jsdom": "^28.1.0",
44
49
  "react": "^16.14.0",
45
50
  "react-dom": "^16.14.0",
46
51
  "redux-saga-test-plan": "^4.0.4",
@@ -48,6 +53,8 @@
48
53
  "semantic-ui-react": "^2.0.3"
49
54
  },
50
55
  "jest": {
56
+ "maxWorkers": "50%",
57
+ "testTimeout": 10000,
51
58
  "moduleDirectories": [
52
59
  "<rootDir>/src",
53
60
  "../../node_modules"
@@ -80,11 +87,11 @@
80
87
  ]
81
88
  },
82
89
  "dependencies": {
83
- "@truedat/auth": "4.44.2",
84
- "@truedat/core": "4.44.2",
90
+ "@truedat/auth": "4.44.5",
91
+ "@truedat/core": "4.44.5",
85
92
  "axios": "^0.19.2",
86
93
  "path-to-regexp": "^1.7.0",
87
- "prop-types": "^15.7.2",
94
+ "prop-types": "^15.8.1",
88
95
  "react-color": "^2.17.3",
89
96
  "react-intl": "^5.20.10",
90
97
  "react-redux": "^7.2.4",
@@ -100,5 +107,5 @@
100
107
  "react-dom": ">= 16.8.6 < 17",
101
108
  "semantic-ui-react": ">= 0.88.2 < 2.1"
102
109
  },
103
- "gitHead": "ca0c5fffcba96736f7a2054f3c37789da8c30a9e"
110
+ "gitHead": "5a339468198c803592b285eddd0dd0c0b0eced93"
104
111
  }
@@ -15,12 +15,13 @@ export const DynamicForm = ({
15
15
  fieldsToOmit,
16
16
  selectedDomain,
17
17
  }) => {
18
+ const domainId = selectedDomain?.id;
18
19
  useEffect(() => {
19
- if (!_.isNil(selectedDomain?.id) && !_.isEmpty(template)) {
20
- onChange(applyTemplate(content, selectedDomain?.id), {});
20
+ if (!_.isNil(domainId) && !_.isEmpty(template)) {
21
+ onChange(applyTemplate(content, domainId), {});
21
22
  }
22
23
  // eslint-disable-next-line react-hooks/exhaustive-deps
23
- }, [applyTemplate, selectedDomain, template]);
24
+ }, [applyTemplate, domainId, template]);
24
25
 
25
26
  if (
26
27
  loading ||
@@ -33,10 +34,7 @@ export const DynamicForm = ({
33
34
 
34
35
  const handleChange = (e, { name, value }) => {
35
36
  e && e.preventDefault();
36
- const newContent = applyTemplate(
37
- { ...content, [name]: value },
38
- selectedDomain?.id
39
- );
37
+ const newContent = applyTemplate({ ...content, [name]: value }, domainId);
40
38
  onChange(newContent, {});
41
39
  };
42
40
 
@@ -49,17 +47,15 @@ export const DynamicForm = ({
49
47
 
50
48
  return (
51
49
  <>
52
- {parsedGroups.map(({ name, fields }, i) => {
53
- return (
54
- <FieldGroupSegment
55
- key={i}
56
- onFieldChange={handleChange}
57
- name={name}
58
- fields={fields}
59
- scope={_.prop("scope")(template)}
60
- />
61
- );
62
- })}
50
+ {parsedGroups.map(({ name, fields }, i) => (
51
+ <FieldGroupSegment
52
+ key={i}
53
+ onFieldChange={handleChange}
54
+ name={name}
55
+ fields={fields}
56
+ scope={_.prop("scope")(template)}
57
+ />
58
+ ))}
63
59
  </>
64
60
  );
65
61
  };
@@ -81,7 +77,13 @@ const makeMapStateToProps = () => {
81
77
  const mapStateToProps = (state, props) => ({
82
78
  applyTemplate: getApplyTemplate(state, props),
83
79
  loading: state.loading,
84
- selectedDomain: state.selectedDomain,
80
+ selectedDomain:
81
+ props.selectedDomain ||
82
+ (!_.isEmpty(state.selectedDomain)
83
+ ? state.selectedDomain
84
+ : _.isNil(_.head(state.selectedDomains))
85
+ ? null
86
+ : { id: _.head(state.selectedDomains) }),
85
87
  template: getTemplate(state, props),
86
88
  });
87
89
  return mapStateToProps;
@@ -0,0 +1,66 @@
1
+ import _ from "lodash/fp";
2
+ import React, { useState } from "react";
3
+ import { TemplateSelector } from "@truedat/core/components";
4
+ import { applyTemplate, validateContent } from "@truedat/df/utils";
5
+ import DynamicForm from "./DynamicForm";
6
+
7
+ export default function SelectableDynamicForm({
8
+ domainIds,
9
+ scope,
10
+ required,
11
+ content,
12
+ header,
13
+ name,
14
+ onChange,
15
+ onNameChange,
16
+ }) {
17
+ const [template, setTemplate] = useState();
18
+ const domain = _.size(domainIds) > 0 ? { id: _.head(domainIds) } : null;
19
+
20
+ const handleChange = (content) => {
21
+ onChange({ content, valid: validateContent(template)(content) });
22
+ };
23
+
24
+ const handleLoad = ({ templates }) => {
25
+ const template = name
26
+ ? _.find(_.propEq("name", name))(templates)
27
+ : _.size(templates) == 1
28
+ ? _.head(templates)
29
+ : null;
30
+ if (template?.name !== name) {
31
+ onNameChange(template?.name);
32
+ }
33
+ handleChange(template ? applyTemplate(template)(content, domain?.id) : {});
34
+ setTemplate(template);
35
+ };
36
+
37
+ const handleSelected = (_e, { template }) => {
38
+ onNameChange(template?.name);
39
+ handleChange(template ? applyTemplate(template)(content, domain?.id) : {});
40
+ setTemplate(template);
41
+ };
42
+
43
+ return (
44
+ <>
45
+ <TemplateSelector
46
+ scope={scope}
47
+ domainIds={domainIds}
48
+ required={required}
49
+ selectedValue={template?.id}
50
+ onChange={handleSelected}
51
+ onLoad={handleLoad}
52
+ />
53
+ {template ? (
54
+ <>
55
+ {header}
56
+ <DynamicForm
57
+ onChange={handleChange}
58
+ content={content}
59
+ template={template}
60
+ selectedDomain={domain}
61
+ />
62
+ </>
63
+ ) : null}
64
+ </>
65
+ );
66
+ }
@@ -0,0 +1,122 @@
1
+ import React from "react";
2
+ import { waitFor, waitForElementToBeRemoved } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { render } from "@truedat/test/render";
5
+
6
+ import {
7
+ errorTemplateMock,
8
+ multipleTemplatesMock,
9
+ singleTemplateMock,
10
+ } from "@truedat/test/mocks";
11
+ import SelectableDynamicForm from "../SelectableDynamicForm";
12
+
13
+ const variables = {
14
+ scope: "foo",
15
+ domainIds: [1],
16
+ };
17
+ const variablesEmptyDomains = { scope: "foo" };
18
+ const props = {
19
+ ...variables,
20
+ onChange: jest.fn(),
21
+ onNameChange: jest.fn(),
22
+ };
23
+
24
+ describe("<SelectableDynamicForm />", () => {
25
+ describe("with multiple templates", () => {
26
+ const renderOpts = {
27
+ mocks: [multipleTemplatesMock(variables)],
28
+ };
29
+
30
+ it("matches the latest snapshot", async () => {
31
+ const { container, queryByText } = render(
32
+ <SelectableDynamicForm {...props} />,
33
+ renderOpts
34
+ );
35
+ await waitForElementToBeRemoved(() => queryByText(/loading/i));
36
+ expect(container).toMatchSnapshot();
37
+ });
38
+
39
+ it("matches the latest snapshot for error query", async () => {
40
+ const renderOpts = {
41
+ mocks: [errorTemplateMock(variables)],
42
+ };
43
+ const { container, queryByText } = render(
44
+ <SelectableDynamicForm {...props} />,
45
+ renderOpts
46
+ );
47
+ await waitForElementToBeRemoved(() => queryByText(/loading/i));
48
+ expect(container).toMatchSnapshot();
49
+ });
50
+
51
+ it("calls onNameChange and onChange functions when selecting template", async () => {
52
+ const onChange = jest.fn();
53
+ const onNameChange = jest.fn();
54
+ const thisProps = {
55
+ ...props,
56
+ onChange,
57
+ onNameChange,
58
+ };
59
+ const { container, findByText, queryByText } = render(
60
+ <SelectableDynamicForm {...thisProps} />,
61
+ renderOpts
62
+ );
63
+
64
+ await waitForElementToBeRemoved(() => queryByText(/loading/i));
65
+ userEvent.click(await findByText("template1"));
66
+
67
+ expect(onChange).toHaveBeenCalledWith({ content: {}, valid: [] });
68
+ expect(onNameChange).toHaveBeenCalledWith("template1");
69
+
70
+ const input = container.querySelector('[name="field1"]');
71
+ userEvent.type(input, "A");
72
+ expect(onChange).toHaveBeenCalledWith({
73
+ content: { field1: "A" },
74
+ valid: [],
75
+ });
76
+ });
77
+ });
78
+
79
+ describe("with a single template", () => {
80
+ const renderOpts = {
81
+ mocks: [singleTemplateMock(variables)],
82
+ };
83
+
84
+ it("matches the latest snapshot", async () => {
85
+ const { container, queryByText } = render(
86
+ <SelectableDynamicForm {...props} />,
87
+ renderOpts
88
+ );
89
+ await waitForElementToBeRemoved(() => queryByText(/loading/i));
90
+ expect(queryByText("template1")).not.toBeInTheDocument();
91
+ expect(container).toMatchSnapshot();
92
+ });
93
+
94
+ it("calls onNameChange function to select the unique template", async () => {
95
+ const onNameChange = jest.fn();
96
+ const thisProps = {
97
+ ...props,
98
+ onNameChange,
99
+ };
100
+ render(<SelectableDynamicForm {...thisProps} />, renderOpts);
101
+ await waitFor(() =>
102
+ expect(onNameChange).toHaveBeenCalledWith("template1")
103
+ );
104
+ });
105
+ });
106
+
107
+ describe("with no domains", () => {
108
+ const renderOpts = {
109
+ mocks: [multipleTemplatesMock(variablesEmptyDomains)],
110
+ };
111
+
112
+ it("matches the latest snapshot", async () => {
113
+ const { container, queryByText } = render(
114
+ <SelectableDynamicForm {...props} />,
115
+ renderOpts
116
+ );
117
+ await waitForElementToBeRemoved(() => queryByText(/loading/i));
118
+ expect(queryByText("template1")).not.toBeInTheDocument();
119
+ expect(container).toMatchSnapshot();
120
+ });
121
+ });
122
+ });
@@ -0,0 +1,114 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`<SelectableDynamicForm /> with a single template matches the latest snapshot 1`] = `
4
+ <div>
5
+ <div
6
+ class="ui segment"
7
+ >
8
+ <h4
9
+ class="ui header"
10
+ >
11
+ g1
12
+ </h4>
13
+ <div
14
+ class="field"
15
+ >
16
+ <label>
17
+ field1
18
+ </label>
19
+ <div
20
+ class="field"
21
+ >
22
+ <div
23
+ class="ui input"
24
+ >
25
+ <input
26
+ name="field1"
27
+ type="text"
28
+ value=""
29
+ />
30
+ </div>
31
+ </div>
32
+ </div>
33
+ </div>
34
+ </div>
35
+ `;
36
+
37
+ exports[`<SelectableDynamicForm /> with multiple templates matches the latest snapshot 1`] = `
38
+ <div>
39
+ <div
40
+ class="field"
41
+ >
42
+ <label>
43
+ Template
44
+ </label>
45
+ <div
46
+ class="field"
47
+ >
48
+ <div
49
+ aria-busy="false"
50
+ aria-expanded="false"
51
+ class="ui search selection dropdown"
52
+ name="template"
53
+ role="combobox"
54
+ >
55
+ <input
56
+ aria-autocomplete="list"
57
+ autocomplete="off"
58
+ class="search"
59
+ tabindex="0"
60
+ type="text"
61
+ value=""
62
+ />
63
+ <div
64
+ aria-atomic="true"
65
+ aria-live="polite"
66
+ class="divider default text"
67
+ role="alert"
68
+ >
69
+ Select a template...
70
+ </div>
71
+ <i
72
+ aria-hidden="true"
73
+ class="dropdown icon"
74
+ />
75
+ <div
76
+ class="menu transition"
77
+ role="listbox"
78
+ >
79
+ <div
80
+ aria-checked="false"
81
+ aria-selected="true"
82
+ class="selected item"
83
+ role="option"
84
+ style="pointer-events: all;"
85
+ >
86
+ <span
87
+ class="text"
88
+ >
89
+ template1
90
+ </span>
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
+ <span
100
+ class="text"
101
+ >
102
+ template2
103
+ </span>
104
+ </div>
105
+ </div>
106
+ </div>
107
+ </div>
108
+ </div>
109
+ </div>
110
+ `;
111
+
112
+ exports[`<SelectableDynamicForm /> with multiple templates matches the latest snapshot for error query 1`] = `<div />`;
113
+
114
+ exports[`<SelectableDynamicForm /> with no domains matches the latest snapshot 1`] = `<div />`;
@@ -3,25 +3,23 @@ import PropTypes from "prop-types";
3
3
  import React from "react";
4
4
  import { DateTimeInput } from "semantic-ui-calendar-react";
5
5
 
6
- export const DateTimeField = ({ field: { name, value }, onChange }) => {
7
- return (
8
- <DateTimeInput
9
- duration={0}
10
- closable
11
- animation={""}
12
- name={name}
13
- dateTimeFormat={"YYYY-MM-DD HH:mm"}
14
- placeholder="Date Time"
15
- value={_.isEmpty(value) ? "" : value}
16
- iconPosition="left"
17
- onChange={(e, { value }) => onChange(e, { name: name, value: value })}
18
- />
19
- );
20
- };
6
+ export const DateTimeField = ({ field: { name, value } = {}, onChange }) => (
7
+ <DateTimeInput
8
+ animation=""
9
+ closable
10
+ dateTimeFormat={"YYYY-MM-DD HH:mm"}
11
+ duration={0}
12
+ iconPosition="left"
13
+ name={name}
14
+ onChange={(e, { value }) => onChange(e, { name: name, value: value })}
15
+ placeholder="Date Time"
16
+ value={value || ""}
17
+ />
18
+ );
21
19
 
22
20
  DateTimeField.propTypes = {
23
21
  field: PropTypes.object,
24
- onChange: PropTypes.func
22
+ onChange: PropTypes.func,
25
23
  };
26
24
 
27
25
  export default DateTimeField;
@@ -137,7 +137,7 @@ export default {
137
137
  "template.error.content.invalidtype":
138
138
  "Field {name} with type {type} already exists in another template",
139
139
  "widget.image.error.too_big":
140
- "Image size is too big. Must be smaller then {size_mb}MB",
140
+ "File is too big. Must be smaller then {size_mb}MB",
141
141
  "widget.image.error.invalidtype": "Invalid File",
142
142
  "widget.copy.modaltitle": "Copy fields",
143
143
  "widget.copy.error.jsonformat": "Error: copy has wrong format",
@@ -0,0 +1,30 @@
1
+ import { selectedDomains } from "../../reducers";
2
+ import { selectDomains, clearSelectedDomains } from "../../routines";
3
+
4
+ const fooState = [{ foo: "bar" }];
5
+
6
+ describe("reducers: selectedDomains", () => {
7
+ const initialState = [];
8
+
9
+ it("should provide the initial state", () => {
10
+ expect(selectedDomains(undefined, {})).toEqual(initialState);
11
+ });
12
+
13
+ it("should handle the clearSelectedDomains.TRIGGER action", () => {
14
+ expect(
15
+ selectedDomains(fooState, { type: clearSelectedDomains.TRIGGER })
16
+ ).toEqual(initialState);
17
+ });
18
+
19
+ it("should handle the selectDomains.TRIGGER action", () => {
20
+ const id = 1;
21
+ const payload = [{ id }];
22
+ expect(
23
+ selectedDomains(fooState, { type: selectDomains.TRIGGER, payload })
24
+ ).toEqual(payload);
25
+ });
26
+
27
+ it("should ignore unknown actions", () => {
28
+ expect(selectedDomains(fooState, { type: "FOO" })).toBe(fooState);
29
+ });
30
+ });
@@ -1,3 +1,4 @@
1
1
  export * from "../templates/reducers";
2
2
  export * from "./dfMessage";
3
3
  export * from "./selectedDomain";
4
+ export * from "./selectedDomains";
@@ -0,0 +1,15 @@
1
+ import _ from "lodash/fp";
2
+ import { selectDomains, clearSelectedDomains } from "../routines";
3
+
4
+ const initialState = [];
5
+
6
+ export const selectedDomains = (state = initialState, { type, payload }) => {
7
+ switch (type) {
8
+ case selectDomains.TRIGGER:
9
+ return payload;
10
+ case clearSelectedDomains.TRIGGER:
11
+ return initialState;
12
+ default:
13
+ return state;
14
+ }
15
+ };
package/src/routines.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { createRoutine } from "redux-saga-routines";
2
2
 
3
- export const selectDomain = createRoutine("SELECT_DOMAIN");
4
3
  export const clearSelectedDomain = createRoutine("CLEAR_SELECTED_DOMAIN");
4
+ export const clearSelectedDomains = createRoutine("CLEAR_SELECTED_DOMAINS");
5
+ export const selectDomain = createRoutine("SELECT_DOMAIN");
6
+ export const selectDomains = createRoutine("SELECT_DOMAINS");
5
7
  export * from "./templates/routines";
@@ -1,42 +1,31 @@
1
1
  import { makeGetApplyTemplate, makeGetTemplate } from "..";
2
2
 
3
+ const templateFromProps = { id: 1 };
4
+ const templateFromState = { id: 2 };
5
+
3
6
  describe("selectors: makeGetTemplate", () => {
4
- const templateFromProps = { id: 1 };
5
- const templateFromState = { id: 2 };
6
7
  const props = { template: templateFromProps };
7
8
  const state = { template: templateFromState };
8
9
  it("template from props prevails to template from state", () => {
9
10
  const getTemplate = makeGetTemplate();
10
11
  const t = getTemplate(state, props);
11
- expect(t).toEqual(templateFromProps);
12
+ expect(t).toBe(templateFromProps);
12
13
  });
13
14
 
14
15
  it("gets template from state if not props provided", () => {
15
16
  const getTemplate = makeGetTemplate();
16
17
  const t = getTemplate(state, {});
17
- expect(t).toEqual(templateFromState);
18
+ expect(t).toBe(templateFromState);
18
19
  });
19
20
  });
20
21
 
21
22
  describe("selectors: makeGetApplyTemplate", () => {
22
- const applyTemplate = jest.fn();
23
- const templateFromProps = { id: 1 };
24
- const templateFromState = { id: 2 };
25
-
26
23
  it("applyTemplate from props prevails to applyTemplate from utils", () => {
24
+ const applyTemplate = jest.fn();
27
25
  const props = { template: templateFromProps, applyTemplate };
28
26
  const state = { template: templateFromState };
29
27
  const getApplyTemplate = makeGetApplyTemplate();
30
28
  getApplyTemplate(state, props);
31
29
  expect(applyTemplate).toHaveBeenCalledTimes(1);
32
30
  });
33
-
34
- it("get applyTemplate utils when not applyTemplate provided in props", () => {
35
- const props = { template: templateFromProps };
36
- const state = { template: templateFromState };
37
- applyTemplate.mockClear();
38
- const getApplyTemplate = makeGetApplyTemplate();
39
- getApplyTemplate(state, props);
40
- expect(applyTemplate).toHaveBeenCalledTimes(0);
41
- });
42
31
  });
@@ -8,6 +8,7 @@ const applyTemplateFromProps = (_state, props) =>
8
8
  _.prop("applyTemplate")(props);
9
9
  const selectTemplate = (propTemplate, stateTemplate) =>
10
10
  _.defaultTo(stateTemplate)(propTemplate);
11
+
11
12
  const selectApplyTemplate = (
12
13
  propTemplate,
13
14
  stateTemplate,
@@ -17,21 +18,13 @@ const selectApplyTemplate = (
17
18
  return _.defaultTo(applyTemplate)(applyTemplateFromProps)(template);
18
19
  };
19
20
 
20
- export const makeGetTemplate = () => {
21
- const getTemplate = createSelector(
22
- templateFromProps,
23
- templateFromState,
24
- selectTemplate
25
- );
26
- return getTemplate;
27
- };
21
+ export const makeGetTemplate = () =>
22
+ createSelector(templateFromProps, templateFromState, selectTemplate);
28
23
 
29
- export const makeGetApplyTemplate = () => {
30
- const getApplyTemplate = createSelector(
24
+ export const makeGetApplyTemplate = () =>
25
+ createSelector(
31
26
  templateFromProps,
32
27
  templateFromState,
33
28
  applyTemplateFromProps,
34
29
  selectApplyTemplate
35
30
  );
36
- return getApplyTemplate;
37
- };
@@ -6,49 +6,61 @@ import { connect } from "react-redux";
6
6
  import { withRouter } from "react-router-dom";
7
7
  import { Loading } from "@truedat/core/components";
8
8
  import { clearTemplate, fetchTemplate } from "../routines";
9
- import { clearSelectedDomain } from "../../routines";
9
+ import { clearSelectedDomain, clearSelectedDomains } from "../../routines";
10
10
 
11
11
  export class TemplateLoader extends React.Component {
12
12
  static propTypes = {
13
- clearTemplate: PropTypes.func,
14
- fetchTemplate: PropTypes.func,
15
13
  clearSelectedDomain: PropTypes.func,
14
+ clearSelectedDomains: PropTypes.func,
15
+ clearTemplate: PropTypes.func,
16
16
  domainId: PropTypes.number,
17
+ domainIds: PropTypes.array,
18
+ fetchTemplate: PropTypes.func,
19
+ match: PropTypes.object.isRequired,
17
20
  templateId: PropTypes.number,
18
21
  templateLoading: PropTypes.bool,
19
- match: PropTypes.object.isRequired,
20
22
  };
21
23
 
22
24
  componentDidMount() {
23
- const { fetchTemplate, domainId, templateId, match } = this.props;
24
- if (match && match.params && match.params.templateId) {
25
+ const { fetchTemplate, domainId, domainIds, templateId, match } =
26
+ this.props;
27
+ if (match?.params?.templateId) {
25
28
  fetchTemplate({ templateId: match.params.templateId });
26
29
  } else if (templateId) {
27
- fetchTemplate({ domainId, templateId });
30
+ fetchTemplate({ domainId, domainIds, templateId });
28
31
  }
29
32
  }
30
33
 
31
34
  componentDidUpdate(prevProps) {
32
- const { clearTemplate, fetchTemplate, domainId, templateId, match } =
33
- this.props;
35
+ const {
36
+ clearTemplate,
37
+ domainId,
38
+ domainIds,
39
+ fetchTemplate,
40
+ match,
41
+ templateId,
42
+ } = this.props;
34
43
  const {
35
44
  domainId: prevDomainId,
36
- templateId: prevTemplateId,
45
+ domainIds: prevDomainIds,
37
46
  match: prevMatch,
47
+ templateId: prevTemplateId,
38
48
  } = prevProps;
39
- if (match && match.params && match.params.templateId) {
40
- if (
41
- prevMatch.params &&
42
- match.params.templateId === prevMatch.params.templateId
43
- ) {
49
+ if (match?.params?.templateId) {
50
+ if (match.params.templateId === prevMatch?.params?.templateId) {
44
51
  // No change
45
52
  } else {
46
53
  fetchTemplate({ templateId: match.params.templateId });
47
54
  }
48
55
  } else if (templateId) {
49
- if (domainId !== prevDomainId || templateId !== prevTemplateId) {
56
+ if (
57
+ domainId !== prevDomainId ||
58
+ domainIds !== prevDomainIds ||
59
+ templateId !== prevTemplateId
60
+ ) {
50
61
  fetchTemplate({
51
62
  domainId,
63
+ domainIds,
52
64
  templateId,
53
65
  });
54
66
  }
@@ -58,9 +70,11 @@ export class TemplateLoader extends React.Component {
58
70
  }
59
71
 
60
72
  componentWillUnmount() {
61
- const { clearTemplate, clearSelectedDomain } = this.props;
73
+ const { clearTemplate, clearSelectedDomain, clearSelectedDomains } =
74
+ this.props;
62
75
  clearTemplate();
63
76
  clearSelectedDomain();
77
+ clearSelectedDomains();
64
78
  }
65
79
 
66
80
  render() {
@@ -75,19 +89,22 @@ export class TemplateLoader extends React.Component {
75
89
 
76
90
  const mapStateToProps = ({
77
91
  selectedDomain,
92
+ selectedDomains,
78
93
  selectedTemplate,
79
94
  templateLoading,
80
95
  }) => ({
81
- templateLoading,
82
96
  domainId: _.prop("id")(selectedDomain),
97
+ domainIds: selectedDomains,
83
98
  templateId: _.prop("id")(selectedTemplate),
99
+ templateLoading,
84
100
  });
85
101
 
86
102
  export default compose(
87
103
  withRouter,
88
104
  connect(mapStateToProps, {
105
+ clearSelectedDomain,
106
+ clearSelectedDomains,
89
107
  clearTemplate,
90
108
  fetchTemplate,
91
- clearSelectedDomain,
92
109
  })
93
110
  )(TemplateLoader);
@@ -7,16 +7,18 @@ describe("<TemplateLoader />", () => {
7
7
  const clearTemplate = jest.fn();
8
8
  const fetchTemplate = jest.fn();
9
9
  const clearSelectedDomain = jest.fn();
10
+ const clearSelectedDomains = jest.fn();
10
11
  const match = {};
11
12
 
12
13
  it("matches the latest snapshot", () => {
13
14
  const templateLoading = true;
14
15
  const props = {
15
16
  clearSelectedDomain,
17
+ clearSelectedDomains,
16
18
  clearTemplate,
17
19
  fetchTemplate,
18
20
  templateLoading,
19
- match
21
+ match,
20
22
  };
21
23
  const wrapper = shallow(<TemplateLoader {...props} />);
22
24
  expect(wrapper).toMatchSnapshot();
@@ -26,10 +28,11 @@ describe("<TemplateLoader />", () => {
26
28
  const templateLoading = true;
27
29
  const props = {
28
30
  clearSelectedDomain,
31
+ clearSelectedDomains,
29
32
  clearTemplate,
30
33
  fetchTemplate,
31
34
  templateLoading,
32
- match
35
+ match,
33
36
  };
34
37
  const wrapper = shallow(<TemplateLoader {...props} />);
35
38
  expect(wrapper.find("Loading").length).toBe(1);
@@ -39,10 +42,11 @@ describe("<TemplateLoader />", () => {
39
42
  const templateLoading = false;
40
43
  const props = {
41
44
  clearSelectedDomain,
45
+ clearSelectedDomains,
42
46
  clearTemplate,
43
47
  fetchTemplate,
44
48
  templateLoading,
45
- match
49
+ match,
46
50
  };
47
51
  const wrapper = shallow(<TemplateLoader {...props} />);
48
52
  expect(wrapper.getElement()).toBeNull();
@@ -52,17 +56,19 @@ describe("<TemplateLoader />", () => {
52
56
  const fetchTemplate = jest.fn();
53
57
  const clearTemplate = jest.fn();
54
58
  const clearSelectedDomain = jest.fn();
59
+ const clearSelectedDomains = jest.fn();
55
60
  const templateLoading = false;
56
61
  const domainId = 1;
57
62
  const templateId = 2;
58
63
  const props = {
59
64
  clearSelectedDomain,
65
+ clearSelectedDomains,
60
66
  fetchTemplate,
61
67
  templateLoading,
62
68
  clearTemplate,
63
69
  domainId,
64
70
  templateId,
65
- match
71
+ match,
66
72
  };
67
73
  jest.spyOn(TemplateLoader.prototype, "componentDidMount");
68
74
  const wrapper = shallow(<TemplateLoader {...props} />);
@@ -72,13 +78,15 @@ describe("<TemplateLoader />", () => {
72
78
  expect(clearTemplate.mock.calls.length).toBe(0);
73
79
  expect(fetchTemplate.mock.calls.length).toBe(1);
74
80
  expect(fetchTemplate.mock.calls[0]).toEqual([
75
- { domainId: 1, templateId: 2 }
81
+ { domainId: 1, templateId: 2 },
76
82
  ]);
77
83
  expect(clearSelectedDomain.mock.calls.length).toBe(0);
84
+ expect(clearSelectedDomains.mock.calls.length).toBe(0);
78
85
  wrapper.unmount();
79
86
  expect(clearTemplate.mock.calls.length).toBe(1);
80
87
  expect(fetchTemplate.mock.calls.length).toBe(1);
81
88
  expect(clearSelectedDomain.mock.calls.length).toBe(1);
89
+ expect(clearSelectedDomains.mock.calls.length).toBe(1);
82
90
  });
83
91
 
84
92
  it("calls fetchTemplate when component updates, clearTemplate when component receives null templateId", () => {
@@ -93,7 +101,7 @@ describe("<TemplateLoader />", () => {
93
101
  clearTemplate,
94
102
  domainId,
95
103
  templateId,
96
- match
104
+ match,
97
105
  };
98
106
  const newProps = { ...props, templateId: 3 };
99
107
  const newNullProps = { ...props, templateId: null };
@@ -109,7 +117,7 @@ describe("<TemplateLoader />", () => {
109
117
  expect(clearTemplate.mock.calls.length).toBe(0);
110
118
  expect(fetchTemplate.mock.calls.length).toBe(2);
111
119
  expect(fetchTemplate.mock.calls[1]).toEqual([
112
- { domainId: 1, templateId: 3 }
120
+ { domainId: 1, templateId: 3 },
113
121
  ]);
114
122
 
115
123
  ReactDOM.render(<TemplateLoader {...newNullProps} />, node);
@@ -1,6 +1,5 @@
1
1
  import TemplateLoader from "./TemplateLoader";
2
2
  import TemplateRoutes from "./TemplateRoutes";
3
- import TemplateSelector from "./TemplateSelector";
4
3
  import TemplatesLoader from "./TemplatesLoader";
5
4
 
6
- export { TemplateLoader, TemplateRoutes, TemplateSelector, TemplatesLoader };
5
+ export { TemplateLoader, TemplateRoutes, TemplatesLoader };
@@ -8,10 +8,14 @@ const toApiPath = compile(API_TEMPLATE);
8
8
 
9
9
  export function* fetchTemplateSaga({ payload }) {
10
10
  try {
11
- const { templateId, domainId } = payload;
11
+ const { templateId, domainId, domainIds } = payload;
12
+ const domainIdsString = domainIds ? domainIds.join(",") : undefined;
12
13
  const url = toApiPath({ id: templateId });
13
14
  yield put(fetchTemplate.request());
14
- const json_opts = { ...JSON_OPTS, params: { domain_id: domainId } };
15
+ const json_opts = {
16
+ ...JSON_OPTS,
17
+ params: { domain_id: domainId, domain_ids: domainIdsString },
18
+ };
15
19
  const { data } = yield call(apiJson, url, json_opts);
16
20
  yield put(fetchTemplate.success({ ...data }));
17
21
  } catch (error) {
@@ -1,59 +0,0 @@
1
- import _ from "lodash/fp";
2
- import React from "react";
3
- import PropTypes from "prop-types";
4
- import { connect } from "react-redux";
5
- import { Form, Label } from "semantic-ui-react";
6
- import { useIntl, FormattedMessage } from "react-intl";
7
-
8
- export const TemplateSelector = ({
9
- hidden,
10
- onChange,
11
- options,
12
- selectedValue,
13
- isOptional,
14
- }) => {
15
- const { formatMessage } = useIntl();
16
- const required = !isOptional;
17
-
18
- return hidden ? null : (
19
- <Form.Field required={required}>
20
- <label>
21
- <FormattedMessage id="template.selector.label" />
22
- {!selectedValue && required ? (
23
- <Label pointing="left">
24
- <FormattedMessage id="template.form.validation.empty_required" />
25
- </Label>
26
- ) : null}
27
- </label>
28
- <Form.Dropdown
29
- placeholder={formatMessage({ id: "template.selector.placeholder" })}
30
- name="template"
31
- search
32
- selection
33
- clearable={isOptional}
34
- options={options}
35
- onChange={onChange}
36
- value={selectedValue || null}
37
- />
38
- </Form.Field>
39
- );
40
- };
41
-
42
- TemplateSelector.propTypes = {
43
- onChange: PropTypes.func,
44
- options: PropTypes.array,
45
- hidden: PropTypes.bool,
46
- selectedValue: PropTypes.number,
47
- isOptional: PropTypes.bool,
48
- };
49
-
50
- const mapStateToProps = ({ templates }) => ({
51
- hidden: _.size(templates) <= 1,
52
- options: _.map(({ label: text, id: value }, i) => ({
53
- key: i,
54
- value,
55
- text,
56
- }))(templates),
57
- });
58
-
59
- export default connect(mapStateToProps)(TemplateSelector);
@@ -1,29 +0,0 @@
1
- import React from "react";
2
- import { shallow } from "enzyme";
3
- import { intl } from "@truedat/test/intl-stub";
4
- import { TemplateSelector } from "../TemplateSelector";
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
-
10
- describe("<TemplateSelector />", () => {
11
- const options = [0, 1, 2, 3].map(v => ({
12
- key: v,
13
- value: v,
14
- text: `text ${v}`
15
- }));
16
- const onChange = jest.fn();
17
- const props = { options, onChange };
18
-
19
- it("matches the latest snapshot", () => {
20
- const wrapper = shallow(<TemplateSelector {...props} />);
21
- expect(wrapper).toMatchSnapshot();
22
- });
23
-
24
- it("renders null if hidden is true", () => {
25
- const props = { options, onChange, hidden: true };
26
- const wrapper = shallow(<TemplateSelector {...props} />);
27
- expect(wrapper.getElement()).toBeNull();
28
- });
29
- });
@@ -1,54 +0,0 @@
1
- // Jest Snapshot v1, https://goo.gl/fbAQLP
2
-
3
- exports[`<TemplateSelector /> matches the latest snapshot 1`] = `
4
- <FormField
5
- required={true}
6
- >
7
- <label>
8
- <MemoizedFormattedMessage
9
- id="template.selector.label"
10
- />
11
- <Label
12
- pointing="left"
13
- >
14
- <MemoizedFormattedMessage
15
- id="template.form.validation.empty_required"
16
- />
17
- </Label>
18
- </label>
19
- <FormDropdown
20
- as={[Function]}
21
- control={[Function]}
22
- name="template"
23
- onChange={[MockFunction]}
24
- options={
25
- Array [
26
- Object {
27
- "key": 0,
28
- "text": "text 0",
29
- "value": 0,
30
- },
31
- Object {
32
- "key": 1,
33
- "text": "text 1",
34
- "value": 1,
35
- },
36
- Object {
37
- "key": 2,
38
- "text": "text 2",
39
- "value": 2,
40
- },
41
- Object {
42
- "key": 3,
43
- "text": "text 3",
44
- "value": 3,
45
- },
46
- ]
47
- }
48
- placeholder="template.selector.placeholder"
49
- search={true}
50
- selection={true}
51
- value={null}
52
- />
53
- </FormField>
54
- `;