@truedat/dd 8.8.7 → 8.9.0

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/dd",
3
- "version": "8.8.7",
3
+ "version": "8.9.0",
4
4
  "description": "Truedat Web Data Dictionary",
5
5
  "sideEffects": false,
6
6
  "module": "src/index.js",
@@ -51,7 +51,7 @@
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.8.7",
54
+ "@truedat/test": "8.9.0",
55
55
  "identity-obj-proxy": "^3.0.0",
56
56
  "jest": "^29.7.0",
57
57
  "redux-saga-test-plan": "^4.0.6"
@@ -86,5 +86,5 @@
86
86
  "svg-pan-zoom": "^3.6.2",
87
87
  "swr": "^2.3.3"
88
88
  },
89
- "gitHead": "ece3f2e166776ff7827198a134bad5a28b029dc7"
89
+ "gitHead": "beff9d7e01556d63d42e54c9f7b641503ee658b2"
90
90
  }
@@ -487,7 +487,12 @@ export const LEGACY_DATA_STRUCTURE_VERSION_QUERY = gql`
487
487
  `;
488
488
 
489
489
  export const buildDSVQuery = (loadStructureMetadata) => gql`
490
- query DataStructureVersion($dataStructureId: ID!, $version: String!) {
490
+ query DataStructureVersion(
491
+ $dataStructureId: ID!,
492
+ $version: String!,
493
+ $includeProfiledFields: Boolean = false
494
+ $skipProfiledFieldsExternalIds: [String!] = []
495
+ ) {
491
496
  dataStructureVersion(dataStructureId: $dataStructureId, version: $version) {
492
497
  id
493
498
  version
@@ -625,6 +630,9 @@ export const buildDSVQuery = (loadStructureMetadata) => gql`
625
630
  out
626
631
  }
627
632
  data_fields_count
633
+ has_profiled_data_fields(
634
+ skip_profiled_fields_external_ids: $skipProfiledFieldsExternalIds
635
+ ) @include(if: $includeProfiledFields)
628
636
  relations {
629
637
  parents {
630
638
  id
@@ -818,6 +826,7 @@ export const DATA_FIELDS_QUERY = gql`
818
826
  $before: Cursor
819
827
  $after: Cursor
820
828
  $filters: DataFieldsFilter
829
+ $includeProfile: Boolean = false
821
830
  ) {
822
831
  dataFields(
823
832
  dataStructureId: $dataStructureId
@@ -844,7 +853,7 @@ export const DATA_FIELDS_QUERY = gql`
844
853
  metadata
845
854
  name
846
855
  type
847
- profile {
856
+ profile @include(if: $includeProfile) {
848
857
  max
849
858
  min
850
859
  most_frequent
@@ -7,13 +7,35 @@ import { clearStructure, fetchStructure } from "../routines";
7
7
 
8
8
  export const StructureLoader = ({ fetchStructure, clearStructure }) => {
9
9
  const { id, version } = useParams();
10
+ const { profiledFields } = useWebContext();
10
11
 
11
12
  const { loadStructureMetadata } = useWebContext();
12
13
 
13
14
  useEffect(() => {
14
- fetchStructure({ id, version, loadStructureMetadata });
15
+ fetchStructure({
16
+ id,
17
+ version,
18
+ loadStructureMetadata,
19
+ dataStructureVersionQueryVariables: {
20
+ includeProfiledFields: profiledFields?.enabled !== false,
21
+ skipProfiledFieldsExternalIds: Array.isArray(
22
+ profiledFields?.disabled_external_ids,
23
+ )
24
+ ? profiledFields.disabled_external_ids.filter(
25
+ (x) => typeof x === "string",
26
+ )
27
+ : [],
28
+ },
29
+ });
15
30
  return () => clearStructure();
16
- }, [id, version, loadStructureMetadata, fetchStructure, clearStructure]);
31
+ }, [
32
+ id,
33
+ version,
34
+ loadStructureMetadata,
35
+ profiledFields,
36
+ fetchStructure,
37
+ clearStructure,
38
+ ]);
17
39
 
18
40
  return null;
19
41
  };
@@ -7,7 +7,6 @@ import { useIntl } from "react-intl";
7
7
  import { HistoryBackButton } from "@truedat/core/components";
8
8
  import { selectTemplate, selectDomains } from "@truedat/df/routines";
9
9
  import { applyTemplate, validateContent } from "@truedat/df/utils";
10
- import { apiJson } from "@truedat/core/services/api";
11
10
  import SuggestionsWidget from "@truedat/ai/components/suggestions/SuggestionsWidget";
12
11
  import { doStructureNoteAction } from "../routines";
13
12
  import { getLatestDfContent } from "../selectors/getSortedStructureNotes";
@@ -29,9 +28,6 @@ export const StructureNotesEdit = ({
29
28
  latestDfContent,
30
29
  }) => {
31
30
  const [content, setContent] = useState(latestDfContent);
32
- const aiSuggestionsUrl = _.prop("_actions.ai_suggestions.href")(
33
- structureNotes,
34
- );
35
31
 
36
32
  const isModification = !_.isEmpty(latestDfContent);
37
33
 
@@ -76,22 +72,28 @@ export const StructureNotesEdit = ({
76
72
  setContent({ ...content, ...suggestions });
77
73
  };
78
74
 
79
- const requestAiSuggestion = (callback) => {
80
- const url = `${aiSuggestionsUrl}?language=${locale}`;
81
- apiJson(url).then(callback);
75
+ const suggestionParams = {
76
+ resource_type: "data_structure",
77
+ resource_body: {
78
+ name: structure.name || "",
79
+ domain: _.get("domains[0].name")(structure) || "",
80
+ type: template?.name,
81
+ },
82
+ domain_ids: structure.domain_ids,
83
+ template_id: template?.id,
84
+ language: locale,
85
+ resource_id: structure.id ?? null,
82
86
  };
83
87
 
84
88
  return (
85
89
  <Segment attached="bottom">
86
90
  <TemplateLoader id="templateloader" />
87
- {aiSuggestionsUrl ? (
88
- <SuggestionsWidget
89
- requestAiSuggestion={requestAiSuggestion}
90
- template={template}
91
- applySuggestions={applySuggestions}
92
- isModification={isModification}
93
- />
94
- ) : null}
91
+ <SuggestionsWidget
92
+ suggestionParams={suggestionParams}
93
+ template={template}
94
+ applySuggestions={applySuggestions}
95
+ isModification={isModification}
96
+ />
95
97
  <Form>
96
98
  <DynamicForm
97
99
  onChange={setContent}
@@ -1,15 +1,15 @@
1
1
  import { render } from "@truedat/test/render";
2
+ import { useWebContext } from "@truedat/core/webContext";
2
3
  import { StructureLoader } from "../StructureLoader";
3
4
 
4
- jest.mock("react-router", () => ({
5
- ...jest.requireActual("react-router"),
6
- useParams: () => ({ id: 1, version: 8 }),
7
- }));
8
5
  jest.mock("@truedat/core/webContext", () => ({
9
6
  useWebContext: jest.fn(),
10
7
  }));
11
8
 
12
- const { useWebContext } = require("@truedat/core/webContext");
9
+ jest.mock("react-router", () => ({
10
+ ...jest.requireActual("react-router"),
11
+ useParams: () => ({ id: 1, version: 8 }),
12
+ }));
13
13
 
14
14
  const props = {
15
15
  clearStructure: jest.fn(),
@@ -17,19 +17,28 @@ const props = {
17
17
  };
18
18
 
19
19
  describe("<StructureLoader />", () => {
20
- it("matches the latest snapshot", () => {
20
+ beforeEach(() => {
21
+ useWebContext.mockReturnValue({});
22
+ props.clearStructure.mockClear();
23
+ props.fetchStructure.mockClear();
24
+ });
25
+
26
+ it("fetches structure without metadata", () => {
21
27
  useWebContext.mockReturnValue({ loadStructureMetadata: false });
22
- const { container } = render(<StructureLoader {...props} />);
23
- expect(container).toMatchSnapshot();
28
+ render(<StructureLoader {...props} />);
24
29
 
25
30
  expect(props.fetchStructure).toHaveBeenCalledWith({
26
31
  id: 1,
27
32
  version: 8,
28
33
  loadStructureMetadata: false,
34
+ dataStructureVersionQueryVariables: {
35
+ includeProfiledFields: true,
36
+ skipProfiledFieldsExternalIds: [],
37
+ },
29
38
  });
30
39
  });
31
40
 
32
- it("matches the latest snapshot", () => {
41
+ it("fetches structure with metadata and profiled fields", () => {
33
42
  useWebContext.mockReturnValue({ loadStructureMetadata: true });
34
43
 
35
44
  render(<StructureLoader {...props} />);
@@ -38,6 +47,30 @@ describe("<StructureLoader />", () => {
38
47
  id: 1,
39
48
  version: 8,
40
49
  loadStructureMetadata: true,
50
+ dataStructureVersionQueryVariables: {
51
+ includeProfiledFields: true,
52
+ skipProfiledFieldsExternalIds: [],
53
+ },
54
+ });
55
+ });
56
+
57
+ it("passes profiled fields query variables from context", () => {
58
+ useWebContext.mockReturnValue({
59
+ profiledFields: {
60
+ enabled: false,
61
+ disabled_external_ids: ["ext_1", 4, null, "ext_2"],
62
+ },
63
+ });
64
+
65
+ render(<StructureLoader {...props} />);
66
+
67
+ expect(props.fetchStructure).toHaveBeenCalledWith({
68
+ id: 1,
69
+ version: 8,
70
+ dataStructureVersionQueryVariables: {
71
+ includeProfiledFields: false,
72
+ skipProfiledFieldsExternalIds: ["ext_1", "ext_2"],
73
+ },
41
74
  });
42
75
  });
43
76
  });
@@ -2,12 +2,22 @@ import userEvent from "@testing-library/user-event";
2
2
  import { waitFor } from "@testing-library/react";
3
3
  import { render, waitForLoad } from "@truedat/test/render";
4
4
  import { applyTemplate } from "@truedat/df/utils";
5
+ import SuggestionsWidget from "@truedat/ai/components/suggestions/SuggestionsWidget";
5
6
  import { StructureNotesEdit } from "../StructureNotesEdit";
6
7
 
8
+ jest.mock("@truedat/ai/components/suggestions/SuggestionsWidget", () => ({
9
+ __esModule: true,
10
+ default: jest.fn(() => null),
11
+ }));
12
+
7
13
  describe("<StructureNotesEdit />", () => {
14
+ beforeEach(() => {
15
+ jest.clearAllMocks();
16
+ });
17
+
8
18
  it("matches the latest snapshot", async () => {
9
19
  const props = {
10
- structure: { id: 1 },
20
+ structure: { id: 1, domain_ids: [1] },
11
21
  template: { id: 2 },
12
22
  doStructureNoteAction: () => {},
13
23
  selectTemplate: () => {},
@@ -24,17 +34,19 @@ describe("<StructureNotesEdit />", () => {
24
34
  expect(rendered.container).toMatchSnapshot();
25
35
  });
26
36
 
27
- it("matches the latest snapshot with ai suggestion action", async () => {
37
+ it("passes data_structure suggestion params to the widget", async () => {
28
38
  const props = {
29
- structure: { id: 1 },
30
- template: { id: 2 },
39
+ structure: {
40
+ id: 7,
41
+ name: "my structure",
42
+ domain_ids: [3],
43
+ domains: [{ name: "my domain" }],
44
+ },
45
+ template: { id: 2, name: "my template" },
31
46
  doStructureNoteAction: () => {},
32
47
  selectTemplate: () => {},
33
48
  selectDomain: () => {},
34
49
  structureNotes: {
35
- _actions: {
36
- ai_suggestions: { href: "suggestion" },
37
- },
38
50
  structureNotes: [],
39
51
  },
40
52
  latestDfContent: { foo: { value: "bar", origin: "user" } },
@@ -43,7 +55,19 @@ describe("<StructureNotesEdit />", () => {
43
55
  };
44
56
  const rendered = render(<StructureNotesEdit {...props} />);
45
57
  await waitForLoad(rendered);
46
- expect(rendered.container).toMatchSnapshot();
58
+
59
+ await waitFor(() => expect(SuggestionsWidget).toHaveBeenCalled());
60
+ expect(SuggestionsWidget.mock.calls[0][0].suggestionParams).toMatchObject({
61
+ resource_type: "data_structure",
62
+ resource_id: 7,
63
+ template_id: 2,
64
+ domain_ids: [3],
65
+ resource_body: {
66
+ name: "my structure",
67
+ domain: "my domain",
68
+ type: "my template",
69
+ },
70
+ });
47
71
  });
48
72
 
49
73
  it("calls doStructureNoteAction when submit is clicked", async () => {
@@ -25,53 +25,3 @@ exports[`<StructureNotesEdit /> matches the latest snapshot 1`] = `
25
25
  </div>
26
26
  </div>
27
27
  `;
28
-
29
- exports[`<StructureNotesEdit /> matches the latest snapshot with ai suggestion action 1`] = `
30
- <div>
31
- <div
32
- class="ui bottom attached segment"
33
- >
34
- <div
35
- class="ui segment"
36
- >
37
- <div
38
- class="ui container"
39
- style="display: flex; justify-content: space-between;"
40
- >
41
- <h4>
42
- <i
43
- aria-hidden="true"
44
- class="lightbulb icon"
45
- />
46
- structure_note.ai_suggestion.header
47
- </h4>
48
- <div
49
- class="ui hidden divider"
50
- />
51
- <button
52
- class="ui button"
53
- >
54
- actions.ai_suggestion
55
- </button>
56
- </div>
57
- </div>
58
- <form
59
- class="ui form"
60
- >
61
- <button
62
- class="ui primary button"
63
- >
64
- actions.save
65
- </button>
66
- <a
67
- class="ui secondary button td-icon-text-control"
68
- data-discover="true"
69
- href="/"
70
- role="button"
71
- >
72
- actions.cancel
73
- </a>
74
- </form>
75
- </div>
76
- </div>
77
- `;
@@ -34,12 +34,14 @@ const structureVersionFields = _.pick([
34
34
  "group",
35
35
  "implementation_count",
36
36
  "data_fields_count",
37
+ "has_profiled_data_fields",
37
38
  "metadata",
38
39
  "name",
39
40
  "note",
40
41
  "parents",
41
42
  "children",
42
43
  "siblings",
44
+ "profile",
43
45
  "source",
44
46
  "system",
45
47
  "type",
@@ -119,6 +119,46 @@ describe("sagas: fetchStructureSaga", () => {
119
119
  }).not.toThrow();
120
120
  });
121
121
 
122
+ it("should forward extra query variables from payload", () => {
123
+ const payload = {
124
+ id,
125
+ dataStructureVersionQueryVariables: {
126
+ includeProfiledFields: false,
127
+ skipProfiledFieldsExternalIds: ["ext_1", 2, null, "ext_2"],
128
+ },
129
+ };
130
+ const meta = { version };
131
+ const variables = {
132
+ includeProfiledFields: false,
133
+ skipProfiledFieldsExternalIds: ["ext_1", 2, null, "ext_2"],
134
+ dataStructureId: id,
135
+ version,
136
+ note_fields: ["note_field"],
137
+ };
138
+
139
+ expect(() => {
140
+ testSaga(fetchStructureSaga, { payload })
141
+ .next()
142
+ .select(getStructureFieldColumns)
143
+ .next(structureFieldColumns)
144
+ .getContext("client")
145
+ .next(client)
146
+ .put(fetchStructure.request())
147
+ .next()
148
+ .call(client.query, {
149
+ fetchPolicy: "network-only",
150
+ query: buildDSVQuery(false),
151
+ variables,
152
+ })
153
+ .next({ data: { dataStructureVersion } })
154
+ .put({ meta, ...fetchStructure.success(data) })
155
+ .next()
156
+ .put(fetchStructure.fulfill())
157
+ .next()
158
+ .isDone();
159
+ }).not.toThrow();
160
+ });
161
+
122
162
  it("should put a failure action when the call returns an error", () => {
123
163
  const message = "Request failed";
124
164
  const error = { message };
@@ -16,13 +16,25 @@ export function* fetchStructureSaga({ payload }) {
16
16
 
17
17
  yield put(fetchStructure.request());
18
18
 
19
- const { id, version = "latest", loadStructureMetadata } = payload;
19
+ const {
20
+ id,
21
+ version = "latest",
22
+ dataStructureVersionQueryVariables,
23
+ loadStructureMetadata,
24
+ } = payload;
25
+ const extra =
26
+ dataStructureVersionQueryVariables &&
27
+ typeof dataStructureVersionQueryVariables === "object"
28
+ ? dataStructureVersionQueryVariables
29
+ : {};
30
+
20
31
  const {
21
32
  data: { dataStructureVersion },
22
33
  } = yield call(client.query, {
23
34
  fetchPolicy: "network-only",
24
35
  query: buildDSVQuery(loadStructureMetadata),
25
36
  variables: {
37
+ ...extra,
26
38
  dataStructureId: id,
27
39
  version,
28
40
  note_fields,
@@ -77,7 +77,7 @@ describe("selectors: getTabVisibility", () => {
77
77
  structure: { profile: { foo: "foo" } },
78
78
  userPermissions,
79
79
  }),
80
- ).toMatchObject({ profile: false });
80
+ ).toMatchObject({ profile: true });
81
81
  expect(
82
82
  getTabVisibility({
83
83
  structureProfile: { patterns: "foo" },
@@ -92,6 +92,33 @@ describe("selectors: getTabVisibility", () => {
92
92
  ).toMatchObject({ profile: true });
93
93
  });
94
94
 
95
+ it("should include profile when profiled descendants exist or own profile", () => {
96
+ const userPermissions = { view_profiling_permission: true };
97
+
98
+ expect(
99
+ getTabVisibility({
100
+ structure: { has_profiled_data_fields: true },
101
+ userPermissions,
102
+ }),
103
+ ).toMatchObject({ profile: true });
104
+
105
+ expect(
106
+ getTabVisibility({
107
+ structure: {
108
+ class: "database",
109
+ has_profiled_data_fields: false,
110
+ },
111
+ userPermissions,
112
+ }),
113
+ ).toMatchObject({ profile: false });
114
+
115
+ expect(
116
+ getTabVisibility({
117
+ structure: { has_profiled_data_fields: true },
118
+ }),
119
+ ).toMatchObject({ profile: false });
120
+ });
121
+
95
122
  it("should include links tab if user has create_link permission", () => {
96
123
  expect(
97
124
  getTabVisibility({ structureActions: { create_link: {} } }),
@@ -100,6 +127,14 @@ describe("selectors: getTabVisibility", () => {
100
127
  });
101
128
  });
102
129
 
130
+ it("should include links if structure has links", () => {
131
+ expect(
132
+ getTabVisibility({ structureActions: { create_link: {} } }),
133
+ ).toMatchObject({
134
+ links: true,
135
+ });
136
+ });
137
+
103
138
  it("should include links tab if structure has links_count > 0", () => {
104
139
  expect(getTabVisibility({ structure: { links_count: 2 } })).toMatchObject({
105
140
  links: true,
@@ -10,13 +10,26 @@ const fieldsTabVisible = (state) => state?.structure?.data_fields_count > 0;
10
10
 
11
11
  const profileTabVisible = ({
12
12
  userPermissions,
13
+ structure,
13
14
  structureFields,
14
15
  structureProfile,
15
- }) =>
16
- _.anyPass([_.prop("view_profiling_permission")])(userPermissions) &&
17
- (_.some(hasProfile)(structureFields) ||
16
+ }) => {
17
+ const hasPermission = _.anyPass([_.prop("view_profiling_permission")])(
18
+ userPermissions,
19
+ );
20
+
21
+ if (!hasPermission) return false;
22
+
23
+ const hasProfiledDescendants = structure?.has_profiled_data_fields === true;
24
+
25
+ return (
26
+ hasProfiledDescendants ||
27
+ hasProfile(structure) ||
28
+ _.some(hasProfile)(structureFields) ||
18
29
  notEmpty(structureProfile?.most_frequent) ||
19
- notEmpty(structureProfile?.patterns));
30
+ notEmpty(structureProfile?.patterns)
31
+ );
32
+ };
20
33
 
21
34
  const linksTabVisible = (state) => {
22
35
  const hasPermission = _.has("create_link", state?.structureActions);
@@ -1,3 +0,0 @@
1
- // Jest Snapshot v1, https://goo.gl/fbAQLP
2
-
3
- exports[`<StructureLoader /> matches the latest snapshot 1`] = `<div />`;