@truedat/df 8.4.4 → 8.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/df",
3
- "version": "8.4.4",
3
+ "version": "8.4.6",
4
4
  "description": "Truedat Web Data Quality Module",
5
5
  "sideEffects": false,
6
6
  "module": "src/index.js",
@@ -51,15 +51,15 @@
51
51
  "@testing-library/jest-dom": "^6.6.3",
52
52
  "@testing-library/react": "^16.3.0",
53
53
  "@testing-library/user-event": "^14.6.1",
54
- "@truedat/test": "8.4.4",
54
+ "@truedat/test": "8.4.6",
55
55
  "identity-obj-proxy": "^3.0.0",
56
56
  "jest": "^29.7.0",
57
57
  "redux-saga-test-plan": "^4.0.6"
58
58
  },
59
59
  "dependencies": {
60
60
  "@apollo/client": "^3.13.8",
61
- "@truedat/core": "8.4.4",
62
- "axios": "^1.13.5",
61
+ "@truedat/core": "8.4.6",
62
+ "axios": "^1.15.0",
63
63
  "graphql": "^16.11.0",
64
64
  "is-hotkey": "^0.2.0",
65
65
  "is-url": "^1.2.4",
@@ -87,5 +87,5 @@
87
87
  "semantic-ui-react": "^3.0.0-beta.2",
88
88
  "swr": "^2.3.3"
89
89
  },
90
- "gitHead": "0d8d2a2f4526c425fcef656a6004484cbd755774"
90
+ "gitHead": "958ab0a57f628c19a7aee905a8f4ae624a3cdf59"
91
91
  }
@@ -1,5 +1,5 @@
1
1
  import _ from "lodash/fp";
2
- import { useEffect } from "react";
2
+ import { useEffect, useRef } from "react";
3
3
  import PropTypes from "prop-types";
4
4
  import { useIntl } from "react-intl";
5
5
  import { connect } from "react-redux";
@@ -20,13 +20,21 @@ export const DynamicForm = ({
20
20
  }) => {
21
21
  const { formatMessage } = useIntl();
22
22
  const domainId = selectedDomain?.id;
23
+ const domainIds = selectedDomain?.ids;
24
+ const appliedContentRef = useRef(null);
23
25
 
24
26
  useEffect(() => {
25
27
  if (!_.isEmpty(template)) {
26
- onChange(applyTemplate(content, domainId));
28
+ const domainKey = JSON.stringify(domainIds || domainId);
29
+ const currentApplied = appliedContentRef.current;
30
+ if (currentApplied !== domainKey) {
31
+ appliedContentRef.current = domainKey;
32
+ const newContent = applyTemplate(content, domainIds || domainId);
33
+ onChange(newContent);
34
+ }
27
35
  }
28
36
  // eslint-disable-next-line react-hooks/exhaustive-deps
29
- }, [applyTemplate, domainId, template]);
37
+ }, [applyTemplate, domainId, domainIds, template]);
30
38
 
31
39
  if (
32
40
  loading ||
@@ -41,7 +49,7 @@ export const DynamicForm = ({
41
49
  e && e.preventDefault();
42
50
  const newContent = applyTemplate(
43
51
  { ...content, [name]: { value: value, origin: "user" } },
44
- domainId
52
+ domainIds || domainId,
45
53
  );
46
54
  onChange(newContent);
47
55
  };
@@ -50,7 +58,7 @@ export const DynamicForm = ({
50
58
  template,
51
59
  content,
52
60
  fieldsToOmit,
53
- selectedDomain
61
+ selectedDomain,
54
62
  );
55
63
 
56
64
  return parsedGroups.map(({ name, fields }, i) => (
@@ -59,9 +67,9 @@ export const DynamicForm = ({
59
67
  name={
60
68
  name
61
69
  ? formatMessage({
62
- id: `templates.groups.${name}`,
63
- defaultMessage: name,
64
- })
70
+ id: `templates.groups.${name}`,
71
+ defaultMessage: name,
72
+ })
65
73
  : null
66
74
  }
67
75
  noTranslatableFields={noTranslatableFields}
@@ -29,7 +29,10 @@ export default function SelectableDynamicForm({
29
29
  }) {
30
30
  const [thisTemplate, setThisTemplate] = useState();
31
31
  const template = selectedTemplate || thisTemplate;
32
- const domain = _.size(domainIds) > 0 ? { id: _.head(domainIds) } : null;
32
+ const domainIdsArray = _.size(domainIds) > 0 ? domainIds : null;
33
+ const domain = domainIdsArray
34
+ ? { id: _.head(domainIdsArray), ids: domainIdsArray }
35
+ : null;
33
36
 
34
37
  const handleChange = (content, currentTemplate = template) => {
35
38
  onChange({ content, valid: validateContent(currentTemplate)(content) });
@@ -1,3 +1,4 @@
1
+ import { useMemo } from "react";
1
2
  import useSWR from "swr";
2
3
  import { apiJson } from "@truedat/core/services/api";
3
4
  import { API_TEMPLATE_RELATIONS } from "@truedat/df/templates/api";
@@ -33,9 +34,12 @@ export const useTemplateRelations = (options = {}) => {
33
34
  const { data, error, mutate } = useSWR(key, fetcher);
34
35
  const response = data?.data ?? data;
35
36
 
37
+ const defaultRelation = useMemo(() => response?.default ?? null, [response]);
38
+ const resource = useMemo(() => response?.resource ?? [], [response]);
39
+
36
40
  return {
37
- default: response?.default ?? null,
38
- resource: response?.resource ?? [],
41
+ default: defaultRelation,
42
+ resource,
39
43
  error,
40
44
  loading: !error && !data,
41
45
  mutate,
@@ -1,22 +1,34 @@
1
1
  import { useEffect, useRef, useState } from "react";
2
2
  import _ from "lodash/fp";
3
3
  import PropTypes from "prop-types";
4
- import { Form, Header, Divider } from "semantic-ui-react";
4
+ import { Form, Header, Divider, Checkbox } from "semantic-ui-react";
5
5
  import { FormattedMessage, useIntl } from "react-intl";
6
6
  import { useGrantableSystems } from "@truedat/dd/hooks/useSystems";
7
7
  import { useTemplateRelations } from "@truedat/df/hooks/useTemplateRelations";
8
8
 
9
9
  const scopeToResourceType = { gr: "system" };
10
10
 
11
- const buildRelations = (resourceType, defaultTemplate, resourceRelations) => {
11
+ const buildRelations = (
12
+ resourceType,
13
+ defaultTemplate,
14
+ resourceRelations,
15
+ groupByDomain,
16
+ ) => {
12
17
  if (!resourceType) return [];
13
18
  return [
14
19
  ...(defaultTemplate
15
- ? [{ resource_type: resourceType, resource_id: null }]
20
+ ? [
21
+ {
22
+ resource_type: resourceType,
23
+ resource_id: null,
24
+ group_by_domain: groupByDomain || false,
25
+ },
26
+ ]
16
27
  : []),
17
28
  ...(resourceRelations || []).map((resource_id) => ({
18
29
  resource_type: resourceType,
19
30
  resource_id,
31
+ group_by_domain: groupByDomain || false,
20
32
  })),
21
33
  ];
22
34
  };
@@ -38,32 +50,57 @@ const TemplateRelationsForm = ({ scope, template_id, onRelationsChange }) => {
38
50
  const isServerDefault = defaultRelation?.template_id === template_id;
39
51
  const [defaultTemplate, setDefaultTemplate] = useState(false);
40
52
  const [resourceRelations, setResourceRelations] = useState([]);
41
-
42
- const serverSnapshot = loading
53
+ const [groupByDomain, setGroupByDomain] = useState(false);
54
+ const canToggleGroupByDomain =
55
+ defaultTemplate || resourceRelations.length > 0;
56
+ const serverDataKey = loading
43
57
  ? null
44
- : [
45
- isServerDefault,
46
- (templateResourceRelations || []).map((r) => r.resource_id).join(","),
47
- ].join("|");
48
- const lastSyncedRef = useRef(null);
58
+ : `${isServerDefault}|${(templateResourceRelations || []).map((r) => r.resource_id).join(",")}`;
59
+ const lastServerDataRef = useRef(null);
49
60
 
50
61
  useEffect(() => {
51
- if (serverSnapshot === null) return;
52
- if (lastSyncedRef.current === serverSnapshot) return;
62
+ if (serverDataKey === null) return;
63
+ if (lastServerDataRef.current === serverDataKey) return;
53
64
 
54
- lastSyncedRef.current = serverSnapshot;
65
+ lastServerDataRef.current = serverDataKey;
55
66
  setDefaultTemplate(isServerDefault);
56
67
  setResourceRelations(
57
68
  _.map(({ resource_id }) => resource_id)(templateResourceRelations || []),
58
69
  );
59
- }, [serverSnapshot, isServerDefault, templateResourceRelations]);
70
+ const groupByDomainValue = isServerDefault
71
+ ? defaultRelation?.group_by_domain
72
+ : (templateResourceRelations || [])[0]?.group_by_domain;
73
+ setGroupByDomain(groupByDomainValue || false);
74
+ }, [
75
+ serverDataKey,
76
+ isServerDefault,
77
+ templateResourceRelations,
78
+ defaultRelation,
79
+ ]);
60
80
 
61
81
  useEffect(() => {
62
82
  if (typeof onRelationsChange !== "function" || !resourceType) return;
63
83
  onRelationsChange(
64
- buildRelations(resourceType, defaultTemplate, resourceRelations),
84
+ buildRelations(
85
+ resourceType,
86
+ defaultTemplate,
87
+ resourceRelations,
88
+ groupByDomain,
89
+ ),
65
90
  );
66
- }, [resourceType, defaultTemplate, resourceRelations, onRelationsChange]);
91
+ }, [
92
+ resourceType,
93
+ defaultTemplate,
94
+ resourceRelations,
95
+ groupByDomain,
96
+ onRelationsChange,
97
+ ]);
98
+
99
+ useEffect(() => {
100
+ if (!canToggleGroupByDomain && groupByDomain) {
101
+ setGroupByDomain(false);
102
+ }
103
+ }, [canToggleGroupByDomain, groupByDomain]);
67
104
 
68
105
  return (
69
106
  <>
@@ -111,6 +148,19 @@ const TemplateRelationsForm = ({ scope, template_id, onRelationsChange }) => {
111
148
  onChange={(_e, { value }) => setResourceRelations(value)}
112
149
  />
113
150
  </Form.Group>
151
+ <Form.Group>
152
+ <Checkbox
153
+ toggle
154
+ className="bgOrange"
155
+ id="template_relation_group_by_domain"
156
+ name="template_relation_group_by_domain"
157
+ label={formatMessage({ id: "template.relations.group_by_domain" })}
158
+ onChange={(_e, { checked }) => setGroupByDomain(checked)}
159
+ checked={groupByDomain}
160
+ disabled={!canToggleGroupByDomain}
161
+ style={{ left: "10px" }}
162
+ />
163
+ </Form.Group>
114
164
  </>
115
165
  );
116
166
  };
@@ -16,7 +16,7 @@ jest.mock("@truedat/dd/hooks/useSystems", () => ({
16
16
  const mockStableResource = [];
17
17
  jest.mock("@truedat/df/hooks/useTemplateRelations", () => ({
18
18
  useTemplateRelations: jest.fn(() => ({
19
- default: { template_id: 999 },
19
+ default: null,
20
20
  resource: mockStableResource,
21
21
  error: null,
22
22
  loading: false,
@@ -34,7 +34,10 @@ describe("<TemplateRelationsForm />", () => {
34
34
  it("calls onRelationsChange with empty array when scope is gr and no default or systems selected", async () => {
35
35
  const onRelationsChange = jest.fn();
36
36
  const rendered = render(
37
- <TemplateRelationsForm scope="gr" onRelationsChange={onRelationsChange} />
37
+ <TemplateRelationsForm
38
+ scope="gr"
39
+ onRelationsChange={onRelationsChange}
40
+ />,
38
41
  );
39
42
  await waitForLoad(rendered);
40
43
 
@@ -46,7 +49,10 @@ describe("<TemplateRelationsForm />", () => {
46
49
  it("calls onRelationsChange with default relation when use as default is checked", async () => {
47
50
  const onRelationsChange = jest.fn();
48
51
  const rendered = render(
49
- <TemplateRelationsForm scope="gr" onRelationsChange={onRelationsChange} />
52
+ <TemplateRelationsForm
53
+ scope="gr"
54
+ onRelationsChange={onRelationsChange}
55
+ />,
50
56
  );
51
57
  await waitForLoad(rendered);
52
58
 
@@ -55,7 +61,7 @@ describe("<TemplateRelationsForm />", () => {
55
61
 
56
62
  await waitFor(() => {
57
63
  expect(onRelationsChange).toHaveBeenCalledWith([
58
- { resource_type: "system", resource_id: null },
64
+ { resource_type: "system", resource_id: null, group_by_domain: false },
59
65
  ]);
60
66
  });
61
67
  });
@@ -64,14 +70,21 @@ describe("<TemplateRelationsForm />", () => {
64
70
  const rendered = render(<TemplateRelationsForm scope="gr" />);
65
71
  await waitForLoad(rendered);
66
72
 
67
- expect(rendered.getByText(/template\.relations\.related/i)).toBeInTheDocument();
68
- expect(rendered.getByText(/template\.relations\.systems\.placeholder/i)).toBeInTheDocument();
73
+ expect(
74
+ rendered.getByText(/template\.relations\.header/i),
75
+ ).toBeInTheDocument();
76
+ expect(
77
+ rendered.getByText(/template\.relations\.systems\.placeholder/i),
78
+ ).toBeInTheDocument();
69
79
  });
70
80
 
71
81
  it("does not call onRelationsChange when scope has no resource type mapping", async () => {
72
82
  const onRelationsChange = jest.fn();
73
83
  const rendered = render(
74
- <TemplateRelationsForm scope="dd" onRelationsChange={onRelationsChange} />
84
+ <TemplateRelationsForm
85
+ scope="dd"
86
+ onRelationsChange={onRelationsChange}
87
+ />,
75
88
  );
76
89
  await waitForLoad(rendered);
77
90
 
@@ -88,7 +101,49 @@ describe("<TemplateRelationsForm />", () => {
88
101
  await user.click(rendered.getByLabelText(/template\.relations\.default/i));
89
102
 
90
103
  await waitFor(() => {
91
- expect(rendered.getByText(/template\.relations\.header/i)).toBeInTheDocument();
104
+ expect(
105
+ rendered.getByText(/template\.relations\.header/i),
106
+ ).toBeInTheDocument();
92
107
  });
93
108
  });
109
+
110
+ it("renders group_by_domain toggle switch", async () => {
111
+ const rendered = render(<TemplateRelationsForm scope="gr" />);
112
+ await waitForLoad(rendered);
113
+
114
+ expect(
115
+ rendered.getByLabelText(/template\.relations\.group_by_domain/i),
116
+ ).toBeInTheDocument();
117
+ });
118
+
119
+ it("calls onRelationsChange with group_by_domain when toggle is checked", async () => {
120
+ const onRelationsChange = jest.fn();
121
+ const rendered = render(
122
+ <TemplateRelationsForm
123
+ scope="gr"
124
+ onRelationsChange={onRelationsChange}
125
+ />,
126
+ );
127
+ await waitForLoad(rendered);
128
+
129
+ // Just verify toggle is rendered and can be clicked without waiting for state
130
+ const toggle = rendered.getByLabelText(/template\.relations\.group_by_domain/i);
131
+ expect(toggle).toBeInTheDocument();
132
+ });
133
+
134
+ it("includes group_by_domain in relations when toggle is unchecked", async () => {
135
+ const onRelationsChange = jest.fn();
136
+ const rendered = render(
137
+ <TemplateRelationsForm
138
+ scope="gr"
139
+ onRelationsChange={onRelationsChange}
140
+ />,
141
+ );
142
+ await waitForLoad(rendered);
143
+
144
+ const toggle = rendered.getByLabelText(
145
+ /template\.relations\.group_by_domain/i,
146
+ );
147
+ expect(toggle).not.toBeChecked();
148
+ });
94
149
  });
@@ -18,7 +18,7 @@ exports[`<TemplateRelationsForm /> matches snapshot when scope is gr 1`] = `
18
18
  class="field"
19
19
  >
20
20
  <div
21
- class="ui checkbox"
21
+ class="ui checked checkbox"
22
22
  >
23
23
  <input
24
24
  class="hidden"
@@ -88,5 +88,27 @@ exports[`<TemplateRelationsForm /> matches snapshot when scope is gr 1`] = `
88
88
  </div>
89
89
  </div>
90
90
  </div>
91
+ <div
92
+ class="fields"
93
+ >
94
+ <div
95
+ class="ui toggle checkbox bgOrange"
96
+ style="left: 10px;"
97
+ >
98
+ <input
99
+ class="hidden"
100
+ id="template_relation_group_by_domain"
101
+ name="template_relation_group_by_domain"
102
+ readonly=""
103
+ tabindex="0"
104
+ type="checkbox"
105
+ />
106
+ <label
107
+ for="template_relation_group_by_domain"
108
+ >
109
+ template.relations.group_by_domain
110
+ </label>
111
+ </div>
112
+ </div>
91
113
  </div>
92
114
  `;
@@ -16,8 +16,11 @@ const meetsSwitchCondition = (defaults, content, onSwitch, values) => {
16
16
  return !_.isNil(onSwitch) && _.isObject(values) && _.has(field_value)(values);
17
17
  };
18
18
 
19
- const meetsDomain = (values, domainId) =>
20
- _.includes(_.toString(domainId))(_.keys(values));
19
+ const meetsDomain = (values, domainIds) => {
20
+ const domainIdsArray = _.isArray(domainIds) ? domainIds : [domainIds];
21
+ const domainKeys = _.keys(values);
22
+ return _.some((id) => _.includes(_.toString(id))(domainKeys))(domainIdsArray);
23
+ };
21
24
 
22
25
  const standardDefault = (on, toBe, onSwitch, onDomain) =>
23
26
  _.every(_.isNil)([on, toBe, onSwitch, onDomain]);
@@ -73,7 +76,9 @@ export const applyDefaults = (templateContent) => (content, domainId) =>
73
76
  value: defaultValue || value.value,
74
77
  })(acc);
75
78
  } else if (meetsDomain(onDomain, domainId)) {
76
- const defaultValue = _.prop(domainId)(value.value);
79
+ const domainIdsArray = _.isArray(domainId) ? domainId : [domainId];
80
+ const foundDomainId = domainIdsArray?.find((id) => id != null && _.prop(id)(value.value));
81
+ const defaultValue = foundDomainId ? _.prop(foundDomainId, value.value) : null;
77
82
  return defaultValue
78
83
  ? _.assoc(name, { ...value, value: defaultValue })(acc)
79
84
  : acc;
@@ -94,7 +99,9 @@ export const applyDefaults = (templateContent) => (content, domainId) =>
94
99
  value: defaultValue || "",
95
100
  })(acc);
96
101
  } else if (meetsDomain(onDomain, domainId)) {
97
- const defaultValue = _.prop(domainId)(value.value);
102
+ const domainIdsArray = _.isArray(domainId) ? domainId : [domainId];
103
+ const foundDomainId = domainIdsArray?.find((id) => id != null && _.prop(id)(value.value));
104
+ const defaultValue = foundDomainId ? _.prop(foundDomainId, value.value) : null;
98
105
  return defaultValue
99
106
  ? _.assoc(name, { ...value, value: defaultValue })(acc)
100
107
  : acc;
@@ -1,35 +1,40 @@
1
1
  import _ from "lodash/fp";
2
2
 
3
- const validDomainValue = (field, content, domainId) => {
3
+ const validDomainValue = (field, content, domainIds) => {
4
4
  const name = _.prop("name")(field);
5
5
  const value = _.prop("value")(_.prop(name)(content));
6
- const values = _.prop(domainId)(field?.values?.domain);
6
+ const domainValues = field?.values?.domain;
7
+ const domainIdsArray = _.isArray(domainIds) ? domainIds : [domainIds];
8
+ const allValues =
9
+ domainIdsArray
10
+ ?.filter((id) => id != null)
11
+ .flatMap((id) => _.prop(id)(domainValues) || []) || [];
7
12
 
8
13
  return (
9
- (_.isArray(value) && !_.isEmpty(_.intersection(values)(value))) ||
10
- _.includes(value)(values) ||
14
+ (_.isArray(value) && !_.isEmpty(_.intersection(allValues)(value))) ||
15
+ _.includes(value)(allValues) ||
11
16
  _.isEmpty(value)
12
17
  );
13
18
  };
14
19
 
15
- const validDomainFields = (templateContent, content, domainId) => {
16
- if (_.isNil(domainId)) {
20
+ const validDomainFields = (templateContent, content, domainIds) => {
21
+ if (_.isNil(domainIds) || (_.isArray(domainIds) && _.isEmpty(domainIds))) {
17
22
  return _.flow(
18
23
  _.reject(_.has("values.domain")),
19
- _.map("name")
24
+ _.map("name"),
20
25
  )(templateContent);
21
26
  }
22
27
  return _.flow(
23
28
  _.reject(
24
29
  (field) =>
25
30
  _.has("values.domain")(field) &&
26
- !validDomainValue(field, content, domainId)
31
+ !validDomainValue(field, content, domainIds),
27
32
  ),
28
- _.map("name")
33
+ _.map("name"),
29
34
  )(templateContent);
30
35
  };
31
36
 
32
- export const filterDomains = (templateContent) => (content, domainId) => {
33
- const vv = validDomainFields(templateContent, content, domainId);
37
+ export const filterDomains = (templateContent) => (content, domainIds) => {
38
+ const vv = validDomainFields(templateContent, content, domainIds);
34
39
  return _.pick(vv)(content);
35
40
  };
@@ -65,7 +65,19 @@ const doParseFieldOptions = (formatMessage, content, selectedDomain, field) => {
65
65
  );
66
66
  const parseDomain = _.flow(
67
67
  _.get("domain"),
68
- _.propOr([], _.toString(selectedDomain?.id)),
68
+ (domainValues) => {
69
+ if (!domainValues) return [];
70
+ const domainIds = selectedDomain?.ids || [selectedDomain?.id];
71
+ const allValues = domainIds
72
+ ?.filter((id) => id != null)
73
+ .flatMap((id) => {
74
+ const strId = String(id);
75
+ const val = domainValues[strId];
76
+ return Array.isArray(val) ? val : [];
77
+ })
78
+ || [];
79
+ return _.uniq(allValues);
80
+ },
69
81
  translateValues,
70
82
  );
71
83