@truedat/dq 4.46.4 → 4.46.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/package.json +5 -5
  3. package/src/api/queries.js +16 -0
  4. package/src/components/ConditionSummary.js +66 -22
  5. package/src/components/ImplementationsRoutes.js +19 -0
  6. package/src/components/NewRuleImplementation.js +23 -6
  7. package/src/components/RuleImplementationHistory.js +84 -0
  8. package/src/components/RuleImplementationHistoryRow.js +50 -0
  9. package/src/components/RuleImplementationLoader.js +25 -37
  10. package/src/components/RuleImplementationTabs.js +15 -4
  11. package/src/components/__tests__/RuleImplementationHistory.spec.js +54 -0
  12. package/src/components/__tests__/RuleImplementationLoader.spec.js +32 -42
  13. package/src/components/__tests__/RuleImplementationTabs.spec.js +1 -0
  14. package/src/components/__tests__/__snapshots__/RuleImplementation.spec.js.snap +6 -0
  15. package/src/components/__tests__/__snapshots__/RuleImplementationHistory.spec.js.snap +142 -0
  16. package/src/components/__tests__/__snapshots__/RuleImplementationLoader.spec.js.snap +15 -1
  17. package/src/components/__tests__/__snapshots__/RuleImplementationTabs.spec.js.snap +6 -0
  18. package/src/components/ruleImplementationForm/FiltersField.js +35 -10
  19. package/src/components/ruleImplementationForm/FiltersFormGroup.js +6 -2
  20. package/src/components/ruleImplementationForm/FiltersGrid.js +14 -4
  21. package/src/components/ruleImplementationForm/__tests__/__snapshots__/FiltersFormGroup.spec.js.snap +7 -1
  22. package/src/components/ruleImplementationForm/__tests__/__snapshots__/FiltersGroup.spec.js.snap +7 -1
  23. package/src/components/ruleImplementationForm/__tests__/__snapshots__/ValueConditions.spec.js.snap +7 -1
  24. package/src/messages/en.js +5 -1
  25. package/src/messages/es.js +6 -1
  26. package/src/routines.js +3 -0
  27. package/src/selectors/getRuleImplementationOperators.js +1 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [4.46.7] 2022-06-20
4
+
5
+ ### Added
6
+
7
+ - [TD-4919] Add ruleImplementation History
8
+
9
+ ## [4.46.5] 2022-06-17
10
+
11
+ ### Added
12
+
13
+ - [TD-4894] Multiple column operator in implementation creation
14
+
3
15
  ## [4.46.3] 2022-06-16
4
16
 
5
17
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/dq",
3
- "version": "4.46.4",
3
+ "version": "4.46.7",
4
4
  "description": "Truedat Web Data Quality Module",
5
5
  "sideEffects": false,
6
6
  "jsnext:main": "src/index.js",
@@ -34,7 +34,7 @@
34
34
  "@testing-library/jest-dom": "^5.16.4",
35
35
  "@testing-library/react": "^12.0.0",
36
36
  "@testing-library/user-event": "^13.2.1",
37
- "@truedat/test": "4.46.4",
37
+ "@truedat/test": "4.46.7",
38
38
  "babel-jest": "^28.1.0",
39
39
  "babel-plugin-dynamic-import-node": "^2.3.3",
40
40
  "babel-plugin-lodash": "^3.3.4",
@@ -88,8 +88,8 @@
88
88
  },
89
89
  "dependencies": {
90
90
  "@apollo/client": "^3.6.4",
91
- "@truedat/core": "4.46.4",
92
- "@truedat/df": "4.46.4",
91
+ "@truedat/core": "4.46.7",
92
+ "@truedat/df": "4.46.7",
93
93
  "axios": "^0.19.2",
94
94
  "graphql": "^15.5.3",
95
95
  "path-to-regexp": "^1.7.0",
@@ -110,5 +110,5 @@
110
110
  "react-dom": ">= 16.8.6 < 17",
111
111
  "semantic-ui-react": ">= 0.88.2 < 2.1"
112
112
  },
113
- "gitHead": "dcd0aa42ffe1fb816945154ec2a2d06889dca7e7"
113
+ "gitHead": "2b1c3f0fcf94a7b510f171c06cfb2e1550a14166"
114
114
  }
@@ -0,0 +1,16 @@
1
+ import { gql } from "@apollo/client";
2
+
3
+ export const IMPLEMENTATION_WITH_VERSIONS_QUERY = gql`
4
+ query Implementation($id: ID!) {
5
+ implementation(id: $id) {
6
+ id
7
+ versions {
8
+ id
9
+ implementation_key
10
+ version
11
+ status
12
+ updated_at
13
+ }
14
+ }
15
+ }
16
+ `;
@@ -8,23 +8,41 @@ import { linkTo } from "@truedat/core/routes";
8
8
 
9
9
  const concatValues = (values, link) => _.join(link)(values);
10
10
 
11
- const FormattedLink = ({ value, operator = {} }) =>
12
- _.propEq("value_type", "field")(operator) ? (
11
+ const LinkToStructure = ({ value }) => {
12
+ return value?.id ? (
13
13
  <Link to={linkTo.STRUCTURE({ id: value.id })}>
14
14
  <span className="highlighted">{`"${
15
15
  !_.isEmpty(value.path) ? path(value) : value.name
16
16
  }"`}</span>{" "}
17
17
  </Link>
18
- ) : (
19
- <span className="highlighted">
20
- <FormattedMessage
21
- id={`ruleImplementation.filtersField.${value.raw}`}
22
- defaultMessage={`"${
23
- _.isArray(value.raw) ? _.join(", ")(value.raw) : value.raw
24
- }"`}
25
- ></FormattedMessage>
26
- </span>
27
- );
18
+ ) : null;
19
+ };
20
+
21
+ const FormattedLink = ({ value, operator = {} }) => {
22
+ switch (operator?.value_type) {
23
+ case "field":
24
+ return <LinkToStructure value={value} />;
25
+ case "field_list":
26
+ return (
27
+ <>
28
+ {_.map.convert({ cap: false })((v, i) => (
29
+ <LinkToStructure key={i} value={v} />
30
+ ))(value)}
31
+ </>
32
+ );
33
+ default:
34
+ return (
35
+ <span className="highlighted">
36
+ <FormattedMessage
37
+ id={`ruleImplementation.filtersField.${value.raw}`}
38
+ defaultMessage={`"${
39
+ _.isArray(value.raw) ? _.join(", ")(value.raw) : value.raw
40
+ }"`}
41
+ ></FormattedMessage>
42
+ </span>
43
+ );
44
+ }
45
+ };
28
46
 
29
47
  FormattedLink.propTypes = {
30
48
  value: PropTypes.object,
@@ -33,26 +51,52 @@ FormattedLink.propTypes = {
33
51
 
34
52
  const nilOrEmpty = (v) => _.isNil(v) || v === "";
35
53
 
36
- const filterNilOrEmpties = (values, keys) =>
37
- _.filter((v) => _.every((k) => !nilOrEmpty(_.prop(k)(v.kv)))(keys))(values);
54
+ const filterNilOrEmpties = (values, keys) => {
55
+ if ("kv" in values) {
56
+ return _.every((k) => !nilOrEmpty(values.kv[k]))(keys) ? values : null;
57
+ }
38
58
 
39
- const valuesFromKeys = (values, keys, optionalKeys, value_modifier) =>
40
- _.flow(
41
- _.map((v) => {
42
- return { kv: _.pick(keys)(v), optv: _.pick(optionalKeys)(v) };
59
+ return _.flow(
60
+ _.map((v) => filterNilOrEmpties(v, keys)),
61
+ _.filter((v) => !!v)
62
+ )(values);
63
+ };
64
+
65
+ const pick = (value, keys, optionalKeys) => ({
66
+ kv: _.pick(keys)(value),
67
+ optv: _.pick(optionalKeys)(value),
68
+ });
69
+
70
+ const convert = (v, index, value_modifier) => {
71
+ if (_.has("kv")(v)) {
72
+ return { ...v.kv, ...v.optv, modifier: _.nth(index)(value_modifier) };
73
+ }
74
+
75
+ return _.map.convert({ cap: false })((v, i) => {
76
+ return convert(v, i, value_modifier);
77
+ })(v);
78
+ };
79
+
80
+ const valuesFromKeys = (values, keys, optionalKeys, value_modifier) => {
81
+ return _.flow(
82
+ _.map((value) => {
83
+ return Array.isArray(value?.fields)
84
+ ? _.map((innerValue) => pick(innerValue, keys, optionalKeys))(
85
+ value?.fields
86
+ )
87
+ : pick(value, keys, optionalKeys);
43
88
  }),
44
89
  (v) => filterNilOrEmpties(v, keys),
45
- _.map.convert({ cap: false })((v, i) => {
46
- return { ...v.kv, ...v.optv, modifier: _.nth(i)(value_modifier) };
47
- })
90
+ (v) => convert(v, 0, value_modifier)
48
91
  )(values);
92
+ };
49
93
 
50
94
  export const empty = (rows) =>
51
95
  rows && _.every((r) => _.isEmpty(r) || _.isNil(r))(rows);
52
96
 
53
97
  export const getValues = ({ value = [], operator = {}, value_modifier }) => {
54
98
  const defaultOrValue = value || [];
55
- return _.propEq("value_type", "field")(operator)
99
+ return ["field", "field_list"].includes(operator?.value_type)
56
100
  ? valuesFromKeys(defaultOrValue, ["id", "name"], ["path"], value_modifier)
57
101
  : valuesFromKeys(defaultOrValue, ["raw"], [], value_modifier);
58
102
  };
@@ -12,6 +12,7 @@ import {
12
12
  IMPLEMENTATION_CONCEPT_LINKS,
13
13
  IMPLEMENTATION_EDIT,
14
14
  IMPLEMENTATION_EVENTS,
15
+ IMPLEMENTATION_HISTORY,
15
16
  IMPLEMENTATION_MOVE,
16
17
  IMPLEMENTATION_NEW,
17
18
  IMPLEMENTATION_NEW_RAW,
@@ -36,6 +37,7 @@ import MoveImplementation from "./MoveImplementation";
36
37
  import NewRuleImplementation from "./NewRuleImplementation";
37
38
  import RuleImplementation from "./RuleImplementation";
38
39
  import RuleImplementationEvents from "./RuleImplementationEvents";
40
+ import RuleImplementationHistory from "./RuleImplementationHistory";
39
41
  import RuleImplementationLoader from "./RuleImplementationLoader";
40
42
  import RuleImplementationProperties from "./RuleImplementationProperties";
41
43
  import RuleImplementationResults from "./RuleImplementationResults";
@@ -312,6 +314,23 @@ export const ImplementationsRoutes = ({
312
314
  </>
313
315
  )}
314
316
  />
317
+ <Route
318
+ exact
319
+ path={IMPLEMENTATION_HISTORY}
320
+ render={() => (
321
+ <>
322
+ <ImplementationCrumbs />
323
+ <Segment>
324
+ {ruleImplementationLoaded ? (
325
+ <RuleImplementation>
326
+ <RuleImplementationHistory />
327
+ </RuleImplementation>
328
+ ) : null}
329
+ </Segment>
330
+ </>
331
+ )}
332
+ />
333
+
315
334
  <Route
316
335
  exact
317
336
  path={IMPLEMENTATION_RESULTS_DETAILS}
@@ -25,6 +25,24 @@ const updateDatasetKey = (data) =>
25
25
  {}
26
26
  )(data);
27
27
 
28
+ // https://stackoverflow.com/a/38417085
29
+ const updateValueKeyValue = (object) => {
30
+ if (object?.id || object?.raw || object?.fields) {
31
+ return object?.id
32
+ ? _.pick(["id", "parent_index"])(object)
33
+ : _.pick(["raw", "fields"])(object);
34
+ }
35
+
36
+ return Object.keys(object).reduce(
37
+ (output, key) => {
38
+ // eslint-disable-next-line fp/no-mutation
39
+ output[key] = updateValueKeyValue(object[key]);
40
+ return output;
41
+ },
42
+ Array.isArray(object) ? [] : {}
43
+ );
44
+ };
45
+
28
46
  const updateConditionValue = (acc, value, key) => {
29
47
  if (key === "structure") {
30
48
  return _.set(key, _.pick(["id", "parent_index"])(value))(acc);
@@ -49,12 +67,10 @@ const updateConditionValue = (acc, value, key) => {
49
67
  )
50
68
  )(acc);
51
69
  if (key == "value")
52
- return _.set(
53
- key,
54
- _.map((v) =>
55
- _.has("id")(v) ? _.pick(["id", "parent_index"])(v) : _.pick(["raw"])(v)
56
- )(value)
57
- )(acc);
70
+ return {
71
+ ...acc,
72
+ value: updateValueKeyValue(value),
73
+ };
58
74
  if (key === "population") return _.set(key, conditionAttributes(value))(acc);
59
75
 
60
76
  return acc;
@@ -103,6 +119,7 @@ const fieldTypeFromStructure = (row, structures, operators, scope) => {
103
119
  field_type,
104
120
  _.prop("operator")(row)
105
121
  );
122
+
106
123
  const updatedRow = {
107
124
  ...row,
108
125
  operator,
@@ -0,0 +1,84 @@
1
+ import _ from "lodash/fp";
2
+ import React from "react";
3
+ import { useParams } from "react-router-dom";
4
+ import { useQuery } from "@apollo/client";
5
+ import PropTypes from "prop-types";
6
+ import { connect } from "react-redux";
7
+ import { Segment, Grid, Table } from "semantic-ui-react";
8
+ import { FormattedMessage } from "react-intl";
9
+ import { Loading } from "@truedat/core/components";
10
+ import { IMPLEMENTATION_WITH_VERSIONS_QUERY } from "../api/queries";
11
+
12
+ import RuleImplementationHistoryRow from "./RuleImplementationHistoryRow";
13
+
14
+ export const RuleImplementationHistory = ({ implementation }) => (
15
+ <Segment attached="bottom">
16
+ <Grid>
17
+ <Grid.Row>
18
+ <Grid.Column>
19
+ {!_.isEmpty(implementation) && (
20
+ <Table selectable>
21
+ <Table.Header>
22
+ <Table.Row>
23
+ <Table.HeaderCell
24
+ content={
25
+ <FormattedMessage id="ruleImplementations.props.implementation_key" />
26
+ }
27
+ />
28
+ <Table.HeaderCell
29
+ content={
30
+ <FormattedMessage id="ruleImplementations.props.status" />
31
+ }
32
+ />
33
+ <Table.HeaderCell
34
+ content={
35
+ <FormattedMessage id="ruleImplementations.props.version" />
36
+ }
37
+ />
38
+ <Table.HeaderCell
39
+ content={
40
+ <FormattedMessage id="ruleImplementations.props.last_change_at" />
41
+ }
42
+ />
43
+ </Table.Row>
44
+ </Table.Header>
45
+ <Table.Body>
46
+ {implementation?.versions.map((item, i) => (
47
+ <RuleImplementationHistoryRow
48
+ key={i}
49
+ {...item}
50
+ active={item.id == implementation.id}
51
+ />
52
+ ))}
53
+ </Table.Body>
54
+ </Table>
55
+ )}
56
+ </Grid.Column>
57
+ </Grid.Row>
58
+ </Grid>
59
+ </Segment>
60
+ );
61
+
62
+ RuleImplementationHistory.propTypes = {
63
+ implementation: PropTypes.object,
64
+ };
65
+
66
+ export const RuleImplementationHistoryLoader = (props) => {
67
+ const { implementation_id: id } = useParams();
68
+ const { loading, error, data } = useQuery(
69
+ IMPLEMENTATION_WITH_VERSIONS_QUERY,
70
+ {
71
+ variables: { id },
72
+ }
73
+ );
74
+ if (error) return null;
75
+ if (loading) return <Loading />;
76
+ const implementation = data?.implementation;
77
+ return (
78
+ <RuleImplementationHistory implementation={implementation} {...props} />
79
+ );
80
+ };
81
+
82
+ export default connect(null, { RuleImplementationHistory })(
83
+ RuleImplementationHistoryLoader
84
+ );
@@ -0,0 +1,50 @@
1
+ import _ from "lodash/fp";
2
+ import React from "react";
3
+ import PropTypes from "prop-types";
4
+ import { useHistory } from "react-router-dom";
5
+ import { Table } from "semantic-ui-react";
6
+ import { FormattedMessage } from "react-intl";
7
+ import { DateTime } from "@truedat/core/components";
8
+ import { linkTo } from "@truedat/core/routes";
9
+
10
+ export const RuleImplementationHistoryRow = ({
11
+ id,
12
+ status,
13
+ updated_at,
14
+ version,
15
+ implementation_key,
16
+ active,
17
+ }) => {
18
+ const history = useHistory();
19
+ return (
20
+ <Table.Row
21
+ active={active}
22
+ onClick={() =>
23
+ history.push(linkTo.IMPLEMENTATION({ implementation_id: id }))
24
+ }
25
+ >
26
+ <Table.Cell content={implementation_key} />
27
+ <Table.Cell
28
+ content={
29
+ <FormattedMessage
30
+ id={`concepts.status.${status}`}
31
+ defaultMessage={status}
32
+ />
33
+ }
34
+ />
35
+ <Table.Cell content={version} />
36
+ <Table.Cell content={<DateTime value={updated_at} />} />
37
+ </Table.Row>
38
+ );
39
+ };
40
+
41
+ RuleImplementationHistoryRow.propTypes = {
42
+ id: PropTypes.number,
43
+ implementation_key: PropTypes.string,
44
+ status: PropTypes.string,
45
+ updated_at: PropTypes.string,
46
+ version: PropTypes.number,
47
+ active: PropTypes.bool,
48
+ };
49
+
50
+ export default RuleImplementationHistoryRow;
@@ -1,48 +1,36 @@
1
- import React from "react";
2
- import { withRouter } from "react-router-dom";
1
+ import React, { useEffect } from "react";
3
2
  import PropTypes from "prop-types";
4
- import { compose } from "redux";
5
3
  import { connect } from "react-redux";
4
+ import { useParams } from "react-router-dom";
6
5
  import { Loading } from "@truedat/core/components";
7
6
  import { clearRuleImplementation, fetchRuleImplementation } from "../routines";
8
7
 
9
- export class RuleImplementationLoader extends React.Component {
10
- static propTypes = {
11
- fetchRuleImplementation: PropTypes.func,
12
- clearRuleImplementation: PropTypes.func,
13
- match: PropTypes.object,
14
- ruleImplementationLoading: PropTypes.bool,
15
- };
8
+ export const RuleImplementationLoader = ({
9
+ clearRuleImplementation,
10
+ fetchRuleImplementation,
11
+ ruleImplementationLoading: loading,
12
+ }) => {
13
+ const { implementation_id } = useParams();
14
+ useEffect(() => {
15
+ fetchRuleImplementation({ id: implementation_id });
16
+ return () => {
17
+ clearRuleImplementation();
18
+ };
19
+ }, [implementation_id, clearRuleImplementation, fetchRuleImplementation]);
20
+ return loading ? <Loading /> : null;
21
+ };
16
22
 
17
- componentDidMount() {
18
- const { fetchRuleImplementation, match } = this.props;
19
- const { implementation_id } = match.params;
20
- if (implementation_id) {
21
- fetchRuleImplementation({ id: implementation_id });
22
- }
23
- }
24
-
25
- componentWillUnmount() {
26
- const { clearRuleImplementation } = this.props;
27
- clearRuleImplementation();
28
- }
29
-
30
- render() {
31
- const { ruleImplementationLoading } = this.props;
32
-
33
- if (ruleImplementationLoading) {
34
- return <Loading />;
35
- } else {
36
- return null;
37
- }
38
- }
39
- }
23
+ RuleImplementationLoader.propTypes = {
24
+ fetchRuleImplementation: PropTypes.func,
25
+ clearRuleImplementation: PropTypes.func,
26
+ ruleImplementationLoading: PropTypes.bool,
27
+ };
40
28
 
41
29
  const mapStateToProps = ({ ruleImplementationLoading }) => ({
42
30
  ruleImplementationLoading,
43
31
  });
44
32
 
45
- export default compose(
46
- withRouter,
47
- connect(mapStateToProps, { clearRuleImplementation, fetchRuleImplementation })
48
- )(RuleImplementationLoader);
33
+ export default connect(mapStateToProps, {
34
+ clearRuleImplementation,
35
+ fetchRuleImplementation,
36
+ })(RuleImplementationLoader);
@@ -7,14 +7,15 @@ import { compose } from "redux";
7
7
  import { connect } from "react-redux";
8
8
  import { FormattedMessage } from "react-intl";
9
9
  import {
10
- IMPLEMENTATION_EVENTS,
11
- IMPLEMENTATION_CONCEPT_LINKS,
12
10
  IMPLEMENTATION_CONCEPT_LINKS_NEW,
13
- IMPLEMENTATION_STRUCTURES,
14
- IMPLEMENTATION_STRUCTURES_NEW,
11
+ IMPLEMENTATION_CONCEPT_LINKS,
12
+ IMPLEMENTATION_EVENTS,
13
+ IMPLEMENTATION_HISTORY,
15
14
  IMPLEMENTATION_MOVE,
16
15
  IMPLEMENTATION_RESULTS_DETAILS,
17
16
  IMPLEMENTATION_RESULTS,
17
+ IMPLEMENTATION_STRUCTURES_NEW,
18
+ IMPLEMENTATION_STRUCTURES,
18
19
  IMPLEMENTATION,
19
20
  linkTo,
20
21
  } from "@truedat/core/routes";
@@ -94,6 +95,16 @@ export const RuleImplementationTabs = ({
94
95
  <FormattedMessage id="tabs.dq.ruleImplementation.details" />
95
96
  </Menu.Item>
96
97
  )}
98
+ <Menu.Item
99
+ active={match.path === IMPLEMENTATION_HISTORY}
100
+ as={Link}
101
+ to={linkTo.IMPLEMENTATION_HISTORY({
102
+ implementation_id: ruleImplementation.id,
103
+ })}
104
+ >
105
+ <FormattedMessage id="tabs.dq.ruleImplementation.history" />
106
+ </Menu.Item>
107
+
97
108
  <Menu.Item
98
109
  active={match.path === IMPLEMENTATION_EVENTS}
99
110
  as={Link}
@@ -0,0 +1,54 @@
1
+ import React from "react";
2
+ import { render } from "@truedat/test/render";
3
+ import { RuleImplementationHistory } from "../RuleImplementationHistory";
4
+
5
+ describe("<RuleImplementationHistory", () => {
6
+ const implementation_id = 23;
7
+ const props = {
8
+ implementation: {
9
+ id: implementation_id,
10
+ versions: [
11
+ {
12
+ id: 34,
13
+ implementation_key: "some implementation key 34",
14
+ version: 3,
15
+ status: "draft",
16
+ updated_at: "2022-02-10T15:22:07Z",
17
+ },
18
+ {
19
+ id: implementation_id,
20
+ implementation_key: "some implementation key " + implementation_id,
21
+ version: 2,
22
+ status: "published",
23
+ updated_at: "2022-02-10T15:22:07Z",
24
+ },
25
+ {
26
+ id: 22,
27
+ implementation_key: "some implementation key 22",
28
+ version: 1,
29
+ status: "versioned",
30
+ updated_at: "2022-02-10T15:22:07Z",
31
+ },
32
+ ],
33
+ },
34
+ };
35
+
36
+ const renderOpts = {
37
+ messages: {
38
+ en: {
39
+ "ruleImplementations.props.implementation_key": "key",
40
+ "ruleImplementations.props.status": "status",
41
+ "ruleImplementations.props.version": "version",
42
+ "ruleImplementations.props.last_change_at": "change",
43
+ },
44
+ },
45
+ };
46
+
47
+ it("matches the latest snapshot", () => {
48
+ const { container } = render(
49
+ <RuleImplementationHistory {...props} />,
50
+ renderOpts
51
+ );
52
+ expect(container).toMatchSnapshot();
53
+ });
54
+ });
@@ -1,67 +1,57 @@
1
1
  import React from "react";
2
- import { shallow } from "enzyme";
2
+ import { render } from "@truedat/test/render";
3
3
  import { RuleImplementationLoader } from "../RuleImplementationLoader";
4
4
 
5
+ const implementation_id = 234;
6
+ jest.mock("react-router-dom", () => ({
7
+ ...jest.requireActual("react-router-dom"),
8
+ useParams: () => ({ implementation_id: implementation_id }),
9
+ }));
10
+
5
11
  describe("<RuleImplementationLoader />", () => {
6
- const match = { params: { implementation_id: 1 } };
7
12
  const clearRuleImplementation = jest.fn();
8
13
  const fetchRuleImplementation = jest.fn();
14
+ const common_props = {
15
+ fetchRuleImplementation,
16
+ clearRuleImplementation,
17
+ };
9
18
 
10
19
  it("matches the latest snapshot", () => {
11
- const ruleImplementationLoading = true;
12
20
  const props = {
13
- fetchRuleImplementation,
14
- clearRuleImplementation,
15
- ruleImplementationLoading,
16
- match
21
+ ...common_props,
22
+ ruleImplementationLoading: true,
17
23
  };
18
- const wrapper = shallow(<RuleImplementationLoader {...props} />);
19
- expect(wrapper).toMatchSnapshot();
24
+ const { container } = render(<RuleImplementationLoader {...props} />);
25
+ expect(container).toMatchSnapshot();
20
26
  });
21
27
 
22
28
  it("renders a loader if rulesLoading is true", () => {
23
- const ruleImplementationLoading = true;
24
29
  const props = {
25
- fetchRuleImplementation,
26
- clearRuleImplementation,
27
- match,
28
- ruleImplementationLoading
30
+ ...common_props,
31
+ ruleImplementationLoading: true,
29
32
  };
30
- const wrapper = shallow(<RuleImplementationLoader {...props} />);
31
- expect(wrapper.find("Loading").length).toBe(1);
33
+ const { container } = render(<RuleImplementationLoader {...props} />);
34
+ expect(container).toMatchSnapshot();
35
+ expect(container.firstChild).not.toBeNull();
32
36
  });
33
37
 
34
38
  it("renders null if ruleImplementationLoading is false", () => {
35
- const ruleImplementationLoading = false;
36
39
  const props = {
37
- fetchRuleImplementation,
38
- clearRuleImplementation,
39
- match,
40
- ruleImplementationLoading
40
+ ...common_props,
41
+ ruleImplementationLoading: false,
41
42
  };
42
- const wrapper = shallow(<RuleImplementationLoader {...props} />);
43
- expect(wrapper.getElement()).toBeNull();
43
+ const { container } = render(<RuleImplementationLoader {...props} />);
44
+ expect(container.firstChild).toBeNull();
44
45
  });
45
46
 
46
47
  it("calls fetchRuleImplementation when component mounts, clearRuleImplementation when component unmounts", () => {
47
- const ruleImplementationLoading = false;
48
- const fetchRuleImplementation = jest.fn();
49
- const clearRuleImplementation = jest.fn();
50
- const props = {
51
- fetchRuleImplementation,
52
- clearRuleImplementation,
53
- match,
54
- ruleImplementationLoading
55
- };
56
- jest.spyOn(RuleImplementationLoader.prototype, "componentDidMount");
57
- const wrapper = shallow(<RuleImplementationLoader {...props} />);
58
- expect(
59
- RuleImplementationLoader.prototype.componentDidMount.mock.calls.length
60
- ).toBe(1);
61
- expect(clearRuleImplementation.mock.calls.length).toBe(0);
62
- expect(fetchRuleImplementation.mock.calls.length).toBe(1);
63
- wrapper.unmount();
64
- expect(clearRuleImplementation.mock.calls.length).toBe(1);
65
- expect(fetchRuleImplementation.mock.calls.length).toBe(1);
48
+ clearRuleImplementation.mockClear();
49
+ fetchRuleImplementation.mockClear();
50
+ const { unmount } = render(<RuleImplementationLoader {...common_props} />);
51
+ expect(fetchRuleImplementation).toHaveBeenCalledWith({
52
+ id: implementation_id,
53
+ });
54
+ unmount();
55
+ expect(clearRuleImplementation).toHaveBeenCalledTimes(1);
66
56
  });
67
57
  });
@@ -12,6 +12,7 @@ describe("<RuleImplementationTabs />", () => {
12
12
  "tabs.dq.ruleImplementation.results": "results",
13
13
  "tabs.dq.ruleImplementation.details": "details",
14
14
  "tabs.dq.ruleImplementation.audit": "audit",
15
+ "tabs.dq.ruleImplementation.history": "history",
15
16
  },
16
17
  },
17
18
  };
@@ -148,6 +148,12 @@ exports[`<RuleImplementation /> matches the latest snapshot 1`] = `
148
148
  >
149
149
  Results
150
150
  </a>
151
+ <a
152
+ class="item"
153
+ href="/implementations/1/history"
154
+ >
155
+ History
156
+ </a>
151
157
  <a
152
158
  class="item"
153
159
  href="/implementations/1/events"
@@ -0,0 +1,142 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`<RuleImplementationHistory matches the latest snapshot 1`] = `
4
+ <div>
5
+ <div
6
+ class="ui bottom attached segment"
7
+ >
8
+ <div
9
+ class="ui grid"
10
+ >
11
+ <div
12
+ class="row"
13
+ >
14
+ <div
15
+ class="column"
16
+ >
17
+ <table
18
+ class="ui selectable table"
19
+ >
20
+ <thead
21
+ class=""
22
+ >
23
+ <tr
24
+ class=""
25
+ >
26
+ <th
27
+ class=""
28
+ >
29
+ key
30
+ </th>
31
+ <th
32
+ class=""
33
+ >
34
+ status
35
+ </th>
36
+ <th
37
+ class=""
38
+ >
39
+ version
40
+ </th>
41
+ <th
42
+ class=""
43
+ >
44
+ change
45
+ </th>
46
+ </tr>
47
+ </thead>
48
+ <tbody
49
+ class=""
50
+ >
51
+ <tr
52
+ class=""
53
+ >
54
+ <td
55
+ class=""
56
+ >
57
+ some implementation key 34
58
+ </td>
59
+ <td
60
+ class=""
61
+ >
62
+ draft
63
+ </td>
64
+ <td
65
+ class=""
66
+ >
67
+ 3
68
+ </td>
69
+ <td
70
+ class=""
71
+ >
72
+ <time
73
+ datetime="1644506527000"
74
+ >
75
+ 2022-02-10 15:22
76
+ </time>
77
+ </td>
78
+ </tr>
79
+ <tr
80
+ class="active"
81
+ >
82
+ <td
83
+ class=""
84
+ >
85
+ some implementation key 23
86
+ </td>
87
+ <td
88
+ class=""
89
+ >
90
+ published
91
+ </td>
92
+ <td
93
+ class=""
94
+ >
95
+ 2
96
+ </td>
97
+ <td
98
+ class=""
99
+ >
100
+ <time
101
+ datetime="1644506527000"
102
+ >
103
+ 2022-02-10 15:22
104
+ </time>
105
+ </td>
106
+ </tr>
107
+ <tr
108
+ class=""
109
+ >
110
+ <td
111
+ class=""
112
+ >
113
+ some implementation key 22
114
+ </td>
115
+ <td
116
+ class=""
117
+ >
118
+ versioned
119
+ </td>
120
+ <td
121
+ class=""
122
+ >
123
+ 1
124
+ </td>
125
+ <td
126
+ class=""
127
+ >
128
+ <time
129
+ datetime="1644506527000"
130
+ >
131
+ 2022-02-10 15:22
132
+ </time>
133
+ </td>
134
+ </tr>
135
+ </tbody>
136
+ </table>
137
+ </div>
138
+ </div>
139
+ </div>
140
+ </div>
141
+ </div>
142
+ `;
@@ -1,3 +1,17 @@
1
1
  // Jest Snapshot v1, https://goo.gl/fbAQLP
2
2
 
3
- exports[`<RuleImplementationLoader /> matches the latest snapshot 1`] = `<Loading />`;
3
+ exports[`<RuleImplementationLoader /> matches the latest snapshot 1`] = `
4
+ <div>
5
+ <div
6
+ class="ui active loader"
7
+ />
8
+ </div>
9
+ `;
10
+
11
+ exports[`<RuleImplementationLoader /> renders a loader if rulesLoading is true 1`] = `
12
+ <div>
13
+ <div
14
+ class="ui active loader"
15
+ />
16
+ </div>
17
+ `;
@@ -29,6 +29,12 @@ exports[`<RuleImplementationTabs /> matches the latest snapshot 1`] = `
29
29
  >
30
30
  results
31
31
  </a>
32
+ <a
33
+ class="item"
34
+ href="/implementations/1/history"
35
+ >
36
+ history
37
+ </a>
32
38
  <a
33
39
  class="item"
34
40
  href="/implementations/1/events"
@@ -48,6 +48,24 @@ export const FiltersField = ({
48
48
  const { value_type, value_type_filter, fixed_values } = operator;
49
49
 
50
50
  const modifierDef = _.find({ name: modifier?.name })(typeCastModifiers);
51
+ const pickFromValue = _.pick(["data_structure_id", "name", "parent_index"]);
52
+
53
+ const getVal = (value) => {
54
+ return _.isNil(_.prop("parent_index")(value))
55
+ ? _.prop("id")(value)
56
+ : `${_.prop("id")(value)}/${_.prop("parent_index")(value)}`;
57
+ };
58
+
59
+ const getValue = (valueOrValues, operator) => {
60
+ const getValueResultado =
61
+ operator?.value_type === "field_list"
62
+ ? (valueOrValues?.fields || []).map((v) => {
63
+ return getVal(v);
64
+ })
65
+ : getVal(valueOrValues);
66
+
67
+ return getValueResultado;
68
+ };
51
69
 
52
70
  switch (value_type) {
53
71
  case "string":
@@ -89,6 +107,7 @@ export const FiltersField = ({
89
107
  case "timestamp":
90
108
  return <DateTimeField label={label} value={value} onChange={onChange} />;
91
109
  case "field":
110
+ case "field_list":
92
111
  const structureFields = getStructureFields(parentStructures);
93
112
  return value_type_filter == "any" ? (
94
113
  <StructureSelectorInputField
@@ -111,24 +130,30 @@ export const FiltersField = ({
111
130
  ) : (
112
131
  <>
113
132
  <StructureFieldsDropdown
133
+ multiple={value_type === "field_list"}
114
134
  label={label}
115
135
  inline={false}
116
136
  parentStructures={parentStructures}
117
137
  structureFields={structureFields}
118
138
  typeCastModifiers={typeCastModifiers}
119
- filters={{ field_type: [fieldType] }}
120
- onSelectField={(value, modifier) =>
139
+ filters={
140
+ value_type === "field_list" ? null : { field_type: [fieldType] }
141
+ }
142
+ onSelectField={(value, modifier) => {
121
143
  onChange(
122
144
  null,
123
- _.pick(["data_structure_id", "name", "parent_index"])(value),
145
+ pickFromValue(value),
124
146
  modifier ? { name: modifier.name } : null
125
- )
126
- }
127
- value={
128
- _.isNil(_.prop("parent_index")(value))
129
- ? _.prop("id")(value)
130
- : `${_.prop("id")(value)}/${_.prop("parent_index")(value)}`
131
- }
147
+ );
148
+ }}
149
+ onSelectFields={(values) => {
150
+ onChange(
151
+ null,
152
+ values.map((value) => pickFromValue(value)),
153
+ null
154
+ );
155
+ }}
156
+ value={getValue(value, operator)}
132
157
  />
133
158
  {modifier && (
134
159
  <FieldModifier
@@ -65,7 +65,9 @@ export const FiltersFormGroup = ({
65
65
  inline={false}
66
66
  parentStructures={parentStructures}
67
67
  structureFields={structureFields}
68
- onSelectField={(value) => onStructureChange(index, value)}
68
+ onSelectField={(value) => {
69
+ return onStructureChange(index, value /*[0]*/);
70
+ }}
69
71
  value={
70
72
  _.isNil(_.prop("parent_index")(clause?.structure))
71
73
  ? _.prop("id")(clause?.structure)
@@ -122,7 +124,9 @@ export const FiltersFormGroup = ({
122
124
  })}
123
125
  options={operatorsOptions}
124
126
  value={getOperatorValue(clause)}
125
- onClick={({ id }) => onOperatorChange(index, id)}
127
+ onClick={(whatever) => {
128
+ onOperatorChange(index, whatever.id);
129
+ }}
126
130
  placeholder={formatMessage({
127
131
  id: "operator.dropdown.placeholder",
128
132
  })}
@@ -56,13 +56,23 @@ export const FiltersGrid = ({
56
56
  });
57
57
  };
58
58
 
59
+ const composeFieldValue = (value) => {
60
+ return {
61
+ id: value.data_structure_id || value.id,
62
+ name: value.name,
63
+ path: value.path,
64
+ parent_index: value.parent_index,
65
+ };
66
+ };
67
+
59
68
  const composeValue = (value_type, value) => {
60
69
  if (value_type == "field") {
70
+ return composeFieldValue(value);
71
+ } else if (value_type === "field_list") {
61
72
  return {
62
- id: value.data_structure_id || value.id,
63
- name: value.name,
64
- path: value.path,
65
- parent_index: value.parent_index,
73
+ fields: _.map((v) => {
74
+ return composeFieldValue(v);
75
+ })(value),
66
76
  };
67
77
  } else {
68
78
  return { raw: value };
@@ -50,7 +50,13 @@ exports[`<FiltersFormGroup /> matches the latest snapshot 1`] = `
50
50
  <div
51
51
  class="menu transition"
52
52
  role="listbox"
53
- />
53
+ >
54
+ <div
55
+ class="message"
56
+ >
57
+ No results found.
58
+ </div>
59
+ </div>
54
60
  </div>
55
61
  </div>
56
62
  <div
@@ -56,7 +56,13 @@ exports[`<FiltersGroup /> matches the latest snapshot when siblings provided 1`]
56
56
  <div
57
57
  class="menu transition"
58
58
  role="listbox"
59
- />
59
+ >
60
+ <div
61
+ class="message"
62
+ >
63
+ No results found.
64
+ </div>
65
+ </div>
60
66
  </div>
61
67
  </div>
62
68
  <div
@@ -56,7 +56,13 @@ exports[`<ValueConditions /> matches the latest snapshot when siblings provided
56
56
  <div
57
57
  class="menu transition"
58
58
  role="listbox"
59
- />
59
+ >
60
+ <div
61
+ class="message"
62
+ >
63
+ No results found.
64
+ </div>
65
+ </div>
60
66
  </div>
61
67
  </div>
62
68
  <div
@@ -423,7 +423,8 @@ export default {
423
423
  "matches regular expression",
424
424
  "ruleImplementation.operator.starts_with": "starts with",
425
425
  "ruleImplementation.operator.starts_with.string": "starts with",
426
- "ruleImplementation.operator.unique": "has unique value",
426
+ "ruleImplementation.operator.unique": "unique",
427
+ "ruleImplementation.operator.unique.field_list": "unique across fields",
427
428
  "ruleImplementation.operator.variation_on_count": "count variation",
428
429
  "ruleImplementation.operator.variation_on_count.string": "count variation",
429
430
  "ruleImplementation.props.esquema": "Structure",
@@ -501,6 +502,7 @@ export default {
501
502
  "ruleImplementations.events.action_restored": "Implementation restored",
502
503
  "ruleImplementations.events.action_updated": "Implementation updated",
503
504
  "ruleImplementations.props.business_concept": "Concept",
505
+ "ruleImplementations.props.last_change_at": "Last change at",
504
506
  "ruleImplementations.props.status": "Status",
505
507
  "ruleImplementations.props.executable": "Executable",
506
508
  "ruleImplementations.props.executable.true": "Executable",
@@ -527,6 +529,7 @@ export default {
527
529
  "ruleImplementations.props.template": "Template",
528
530
  "ruleImplementations.props.rule_template": "Rule template",
529
531
  "ruleImplementations.props.implementation_template": "Implementation template",
532
+ "ruleImplementations.props.version": "Version",
530
533
  "ruleImplementations.retrieved.results": "{count} implementations found",
531
534
  "ruleImplementations.search.results.empty": "No implementations found",
532
535
  "ruleImplementations.searching": "Searching implementations",
@@ -599,6 +602,7 @@ export default {
599
602
  "tabs.dq.ruleImplementation": "Implementation",
600
603
  "tabs.dq.ruleImplementation.audit": "Audit",
601
604
  "tabs.dq.ruleImplementation.details": "Details",
605
+ "tabs.dq.ruleImplementation.history": "History",
602
606
  "tabs.dq.ruleImplementation.results": "Results",
603
607
  "tabs.dq.ruleImplementations": "Implementations",
604
608
  "tabs.dq.ruleImplementationResult.info": "Information",
@@ -435,7 +435,8 @@ export default {
435
435
  "ruleImplementation.operator.regex_format": "cumple la expresión regular",
436
436
  "ruleImplementation.operator.starts_with.string": "empieza por",
437
437
  "ruleImplementation.operator.starts_with": "empieza por",
438
- "ruleImplementation.operator.unique": "tiene valor único",
438
+ "ruleImplementation.operator.unique": "único",
439
+ "ruleImplementation.operator.unique.field_list": "único en conjunto",
439
440
  "ruleImplementation.operator.variation_on_count.string": "variación conteo",
440
441
  "ruleImplementation.operator.variation_on_count": "variación conteo",
441
442
  "ruleImplementation.props.esquema.placeholder": "añade una estructura",
@@ -516,6 +517,7 @@ export default {
516
517
  "ruleImplementations.events.action_restored": "Implementación restaurada",
517
518
  "ruleImplementations.events.action_updated": "Implementación actualizada",
518
519
  "ruleImplementations.props.business_concept": "Concepto",
520
+ "ruleImplementations.props.last_change_at": "Fecha de última modificación",
519
521
  "ruleImplementations.props.executable.false": "Interna",
520
522
  "ruleImplementations.props.executable.true": "Ejecutable",
521
523
  "ruleImplementations.props.executable": "Ejecutable",
@@ -545,6 +547,8 @@ export default {
545
547
  "ruleImplementations.props.rule_template": "Plantilla de regla",
546
548
  "ruleImplementations.props.implementation_template":
547
549
  "Plantilla de implementación",
550
+
551
+ "ruleImplementations.props.version": "Versión",
548
552
  "ruleImplementations.retrieved.results":
549
553
  "{count} implementaciones encontradas",
550
554
  "ruleImplementations.search.results.empty":
@@ -623,6 +627,7 @@ export default {
623
627
  "tabs.dq.rule.audit": "Auditoría",
624
628
  "tabs.dq.ruleImplementation.audit": "Auditoría",
625
629
  "tabs.dq.ruleImplementation.details": "Detalles",
630
+ "tabs.dq.ruleImplementation.history": "Historial",
626
631
  "tabs.dq.ruleImplementation.results": "Resultados",
627
632
  "tabs.dq.ruleImplementation": "Implementación",
628
633
  "tabs.dq.ruleImplementations": "Implementaciones",
package/src/routines.js CHANGED
@@ -61,6 +61,9 @@ export const fetchRuleImplementation = createRoutine(
61
61
  export const fetchRuleImplementations = createRoutine(
62
62
  "FETCH_RULE_IMPLEMENTATIONS"
63
63
  );
64
+ export const fetchRuleImplementationV2 = createRoutine(
65
+ "FETCH_RULE_IMPLEMENTATION_V2"
66
+ );
64
67
  export const fetchRules = createRoutine("FETCH_RULES");
65
68
  export const fetchSegmentResults = createRoutine("FETCH_SEGMENT_RESULTS");
66
69
  export const openImplementationFilter = createRoutine(
@@ -5,6 +5,7 @@ const defaultOperators = {
5
5
  any: {
6
6
  operators: [
7
7
  { name: "unique", scope: "validation" },
8
+ { name: "unique", value_type: "field_list" },
8
9
  { name: "not_empty" },
9
10
  { name: "empty" },
10
11
  {