@truedat/dd 5.3.0 → 5.3.1

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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [5.3.1] 2023-03-11
4
+
5
+ ### Added
6
+
7
+ - [TD-5509] Structure to structure link form
8
+
3
9
  ## [5.3.0] 2023-03-08
4
10
 
5
11
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/dd",
3
- "version": "5.3.0",
3
+ "version": "5.3.1",
4
4
  "description": "Truedat Web Data Dictionary",
5
5
  "sideEffects": false,
6
6
  "jsnext:main": "src/index.js",
@@ -34,7 +34,7 @@
34
34
  "@testing-library/jest-dom": "^5.16.5",
35
35
  "@testing-library/react": "^12.0.0",
36
36
  "@testing-library/user-event": "^13.2.1",
37
- "@truedat/test": "5.2.4",
37
+ "@truedat/test": "5.3.1",
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",
@@ -87,9 +87,9 @@
87
87
  },
88
88
  "dependencies": {
89
89
  "@apollo/client": "^3.7.1",
90
- "@truedat/auth": "5.3.0",
91
- "@truedat/core": "5.3.0",
92
- "@truedat/df": "5.3.0",
90
+ "@truedat/auth": "5.3.1",
91
+ "@truedat/core": "5.3.1",
92
+ "@truedat/df": "5.3.1",
93
93
  "lodash": "^4.17.21",
94
94
  "moment": "^2.29.4",
95
95
  "path-to-regexp": "^1.7.0",
@@ -114,5 +114,5 @@
114
114
  "react-dom": ">= 16.8.6 < 17",
115
115
  "semantic-ui-react": ">= 2.0.3 < 2.2"
116
116
  },
117
- "gitHead": "4893c314084a4ea7ca48a4e251d4e47565f3f030"
117
+ "gitHead": "6f7ae72a2abda5e41480fa5254c3c37447821a45"
118
118
  }
@@ -1,6 +1,15 @@
1
1
  import { gql } from "@apollo/client";
2
2
  import { TAG_DETAIL } from "./fragments";
3
3
 
4
+ export const LINK_LABELS_QUERY = gql`
5
+ query StructureToStructureLabels {
6
+ labels {
7
+ id
8
+ name
9
+ }
10
+ }
11
+ `;
12
+
4
13
  export const TAG_QUERY = gql`
5
14
  query Tag($id: ID!) {
6
15
  tag(id: $id) {
@@ -166,6 +175,8 @@ export const STRUCTURE_STRUCTURE_LINKS_QUERY = gql`
166
175
  dataStructure(id: $id) {
167
176
  id
168
177
  dataStructureLinks {
178
+ _actions
179
+ id
169
180
  labels {
170
181
  name
171
182
  }
package/src/api.js CHANGED
@@ -30,6 +30,9 @@ const API_PROFILE_GROUPS = "/api/profile_execution_groups";
30
30
  const API_SYSTEM = "/api/systems/:id";
31
31
  const API_SYSTEMS = "/api/systems";
32
32
  const API_SYSTEM_STRUCTURES = "/api/systems/:id/structures";
33
+ const API_STRUCTURE_TO_STRUCTURE_LINKS = "/api/data_structure_links";
34
+ const API_STRUCTURE_TO_STRUCTURE_LINK =
35
+ "/api/data_structures/structure_links/source/:sourceStructureId/target/:targetStructureId";
33
36
  const API_STRUCTURE_TYPE = "/api/data_structure_types/:id";
34
37
  const API_STRUCTURE_TYPES = "/api/data_structure_types";
35
38
  const API_STRUCTURES_DOMAINS_UPLOAD =
@@ -87,4 +90,6 @@ export {
87
90
  API_STRUCTURES_UPLOAD_EVENTS,
88
91
  API_STRUCTURE_NOTE,
89
92
  API_STRUCTURE_NOTES,
93
+ API_STRUCTURE_TO_STRUCTURE_LINKS,
94
+ API_STRUCTURE_TO_STRUCTURE_LINK,
90
95
  };
@@ -0,0 +1,74 @@
1
+ import _ from "lodash/fp";
2
+ import React from "react";
3
+ import PropTypes from "prop-types";
4
+ import { Form } from "semantic-ui-react";
5
+ import { useIntl, FormattedMessage } from "react-intl";
6
+ import { useQuery } from "@apollo/client";
7
+ import { LINK_LABELS_QUERY } from "../api/queries";
8
+
9
+ const makeOptions = (labels) =>
10
+ _.flow(
11
+ _.sortBy("name"),
12
+ _.map(({ id, name }) => ({
13
+ key: id,
14
+ value: id,
15
+ text: name,
16
+ }))
17
+ )(labels);
18
+
19
+ export const LinkLabelsSelector = ({
20
+ labels,
21
+ loading,
22
+ onChange,
23
+ selectedLabelIds,
24
+ }) => {
25
+ const { formatMessage } = useIntl();
26
+ const options = makeOptions(labels);
27
+
28
+ return _.isEmpty(labels) ? null : (
29
+ <Form.Field>
30
+ <label>
31
+ <FormattedMessage id="structureLink.selector.label" />
32
+ </label>
33
+
34
+ <Form.Dropdown
35
+ loading={loading}
36
+ multiple
37
+ name="link-label-selector"
38
+ onChange={(_e, { value: labels }) => {
39
+ onChange(labels);
40
+ }}
41
+ options={options}
42
+ placeholder={formatMessage({
43
+ id: loading ? "loading" : "structureLink.selector.placeholder",
44
+ })}
45
+ search
46
+ selection
47
+ value={selectedLabelIds}
48
+ />
49
+ </Form.Field>
50
+ );
51
+ };
52
+
53
+ LinkLabelsSelector.propTypes = {
54
+ labels: PropTypes.array,
55
+ loading: PropTypes.bool,
56
+ onChange: PropTypes.func,
57
+ selectedLabelIds: PropTypes.array,
58
+ };
59
+
60
+ export const LinkLabelsSelectorLoader = (props) => {
61
+ const { error, data } = useQuery(LINK_LABELS_QUERY, {
62
+ fetchPolicy: "cache-and-network",
63
+ });
64
+ if (error) return null;
65
+ const labels = data?.labels || [];
66
+ return <LinkLabelsSelector {...props} labels={labels} />;
67
+ };
68
+
69
+ LinkLabelsSelectorLoader.propTypes = {
70
+ onChange: PropTypes.func,
71
+ selectedLabelIds: PropTypes.array,
72
+ };
73
+
74
+ export default LinkLabelsSelectorLoader;
@@ -0,0 +1,95 @@
1
+ import _ from "lodash/fp";
2
+ import React, { Suspense } from "react";
3
+ import { useForm, Controller } from "react-hook-form";
4
+ import { Button, Form } from "semantic-ui-react";
5
+ import { connect } from "react-redux";
6
+ import PropTypes from "prop-types";
7
+ import { useIntl } from "react-intl";
8
+ import { HistoryBackButton } from "@truedat/core/components";
9
+ import { linkStructureToStructure as linkStructureToStructureRoutine } from "../routines";
10
+ import LinkLabelsSelector from "./LinkLabelsSelector";
11
+
12
+ const StructureSelector = React.lazy(() =>
13
+ import("@truedat/dd/components/StructureSelector")
14
+ );
15
+
16
+ export const StructureStructureForm = ({
17
+ sourceStructure,
18
+ linkStructureToStructure,
19
+ }) => {
20
+ const { formatMessage } = useIntl();
21
+
22
+ const { handleSubmit, control, formState } = useForm({
23
+ mode: "all",
24
+ defaultValues: {
25
+ sourceStructureId: sourceStructure.id,
26
+ targetStructureId: null,
27
+ labelIds: [],
28
+ },
29
+ });
30
+ const { isDirty, isValid } = formState;
31
+
32
+ return (
33
+ <Suspense fallback={null}>
34
+ <Form
35
+ onSubmit={handleSubmit((data) => {
36
+ linkStructureToStructure(data);
37
+ })}
38
+ >
39
+ <Controller
40
+ control={control}
41
+ name="labelIds"
42
+ render={({ field: { onChange, value: selectedLabelIds } }) => (
43
+ <LinkLabelsSelector
44
+ onChange={onChange}
45
+ selectedLabelIds={selectedLabelIds}
46
+ />
47
+ )}
48
+ />
49
+
50
+ <Controller
51
+ control={control}
52
+ name="targetStructureId"
53
+ rules={{ required: true }}
54
+ render={({ field: { onChange } }) => (
55
+ <StructureSelector
56
+ onSelect={({ id }) => {
57
+ onChange(id);
58
+ }}
59
+ />
60
+ )}
61
+ />
62
+
63
+ <div className="actions">
64
+ <HistoryBackButton
65
+ content={formatMessage({ id: "actions.cancel" })}
66
+ />
67
+ <Button
68
+ type="submit"
69
+ disabled={!isDirty || !isValid}
70
+ primary
71
+ content={formatMessage({ id: "actions.save" })}
72
+ />
73
+ </div>
74
+ </Form>
75
+ </Suspense>
76
+ );
77
+ };
78
+
79
+ StructureStructureForm.propTypes = {
80
+ sourceStructure: PropTypes.object,
81
+ linkStructureToStructure: PropTypes.func,
82
+ };
83
+
84
+ const mapDispatchToProps = {
85
+ linkStructureToStructure: linkStructureToStructureRoutine,
86
+ };
87
+
88
+ const mapStateToProps = ({ structure }) => ({
89
+ sourceStructure: structure,
90
+ });
91
+
92
+ export default connect(
93
+ mapStateToProps,
94
+ mapDispatchToProps
95
+ )(StructureStructureForm);
@@ -2,13 +2,18 @@ import _ from "lodash/fp";
2
2
  import React from "react";
3
3
  import PropTypes from "prop-types";
4
4
  import { FormattedMessage } from "react-intl";
5
- import { Table, Grid } from "semantic-ui-react";
5
+ import { Button, Table, Grid } from "semantic-ui-react";
6
+ import { Link } from "react-router-dom";
7
+ import { linkTo } from "@truedat/core/routes";
6
8
  import { columnDecorator } from "@truedat/core/services";
7
9
  import { Loading } from "@truedat/core/components";
8
10
  import { useQuery } from "@apollo/client";
9
11
  import { useParams } from "react-router-dom";
12
+ import { connect } from "react-redux";
13
+ import { ConfirmModal } from "@truedat/core/components";
10
14
  import { StructureCrumbs } from "../components/StructureCrumbs";
11
15
  import { STRUCTURE_STRUCTURE_LINKS_QUERY } from "../api/queries";
16
+ import { deleteStructureStructureLink as deleteStructureStructureLinkRoutine } from "../routines";
12
17
 
13
18
  // An arrow with labels
14
19
  export const Edge = ({ structureId, structureLink }) => (
@@ -68,12 +73,51 @@ Edge.propTypes = {
68
73
  structureLink: PropTypes.object,
69
74
  };
70
75
 
76
+ export const DeleteLink = ({ deleteStructureStructureLink, structureLink }) => {
77
+ return !structureLink._actions.delete_struct_to_struct_link ? null : (
78
+ <ConfirmModal
79
+ icon="trash"
80
+ trigger={
81
+ <Button
82
+ id={`delete-link-${structureLink.source.id}-${structureLink.target.id}`}
83
+ icon="trash alternate"
84
+ color="red"
85
+ />
86
+ }
87
+ header={
88
+ <FormattedMessage id="relations.actions.data_field.delete.confirmation.header" />
89
+ }
90
+ content={
91
+ <FormattedMessage id="relations.actions.data_field.delete.confirmation.content" />
92
+ }
93
+ onConfirm={() =>
94
+ _.flow(
95
+ ({
96
+ source: { id: sourceStructureId },
97
+ target: { id: targetStructureId },
98
+ id: dataStructureLinkId,
99
+ }) => ({ dataStructureLinkId, sourceStructureId, targetStructureId }),
100
+ (destructuredLink) => deleteStructureStructureLink(destructuredLink)
101
+ )(structureLink)
102
+ }
103
+ onOpen={(e) => e.stopPropagation()}
104
+ onClose={(e) => e.stopPropagation()}
105
+ />
106
+ );
107
+ };
108
+
109
+ DeleteLink.propTypes = {
110
+ structureLink: PropTypes.object,
111
+ deleteStructureStructureLink: PropTypes.func,
112
+ };
113
+
71
114
  export const StructureStructureLinks = ({
72
115
  structureId,
73
116
  currentVersion,
74
117
  structureLinks,
118
+ canLinkStructureToStructure,
75
119
  }) => {
76
- const columns = [
120
+ const fixedColumns = [
77
121
  {
78
122
  header: "structureLink.currentStructure",
79
123
  fieldSelector: () => currentVersion.name,
@@ -107,6 +151,21 @@ export const StructureStructureLinks = ({
107
151
  },
108
152
  ];
109
153
 
154
+ const DeleteLinkConnected = connect(null, {
155
+ deleteStructureStructureLink: deleteStructureStructureLinkRoutine,
156
+ })(DeleteLink);
157
+
158
+ const deleteButtonColumn = {
159
+ textAlign: "center",
160
+ name: "_actions",
161
+ fieldSelector: (structureLink) => structureLink,
162
+ fieldDecorator: (structureLink) =>
163
+ React.createElement(DeleteLinkConnected, { structureLink }),
164
+ };
165
+ const columns = canLinkStructureToStructure
166
+ ? [...fixedColumns, deleteButtonColumn]
167
+ : fixedColumns;
168
+
110
169
  const headerRow = columns.map(({ header }, key) => (
111
170
  <Table.HeaderCell
112
171
  key={key}
@@ -128,6 +187,17 @@ export const StructureStructureLinks = ({
128
187
 
129
188
  return (
130
189
  <Grid>
190
+ {structureId && canLinkStructureToStructure && (
191
+ <Grid.Column width={16}>
192
+ <Button
193
+ floated="right"
194
+ primary
195
+ as={Link}
196
+ to={linkTo.STRUCTURE_STRUCTURE_LINKS_NEW({ id: structureId })}
197
+ content={<FormattedMessage id="links.actions.create" />}
198
+ />
199
+ </Grid.Column>
200
+ )}
131
201
  {!_.isEmpty(structureLinks) && (
132
202
  <Grid.Column width={16}>
133
203
  <Table
@@ -145,8 +215,23 @@ StructureStructureLinks.propTypes = {
145
215
  structureId: PropTypes.string,
146
216
  currentVersion: PropTypes.object,
147
217
  structureLinks: PropTypes.array,
218
+ canLinkStructureToStructure: PropTypes.bool,
219
+ };
220
+
221
+ const mapStateToProps = (state) => {
222
+ const { structureActions } = state;
223
+ return {
224
+ canLinkStructureToStructure: _.has(
225
+ "create_struct_to_struct_link",
226
+ structureActions
227
+ ),
228
+ };
148
229
  };
149
230
 
231
+ const StructureStructureLinksConnected = connect(mapStateToProps)(
232
+ StructureStructureLinks
233
+ );
234
+
150
235
  export const StructuresStructureLinksLoader = () => {
151
236
  const { id } = useParams();
152
237
  const { loading, error, data } = useQuery(STRUCTURE_STRUCTURE_LINKS_QUERY, {
@@ -155,8 +240,9 @@ export const StructuresStructureLinksLoader = () => {
155
240
  });
156
241
  if (error) return null;
157
242
  if (loading) return <Loading />;
243
+
158
244
  return (
159
- <StructureStructureLinks
245
+ <StructureStructureLinksConnected
160
246
  structureId={data?.dataStructure?.id}
161
247
  structureLinks={data?.dataStructure?.dataStructureLinks}
162
248
  currentVersion={data?.dataStructure?.currentVersion}
@@ -5,6 +5,7 @@ import { Route, Switch } from "react-router-dom";
5
5
  import { ErrorBoundary } from "@truedat/core/components";
6
6
  import {
7
7
  STRUCTURE_LINKS_NEW,
8
+ STRUCTURE_STRUCTURE_LINKS_NEW,
8
9
  STRUCTURE_NOTES_EDIT,
9
10
  } from "@truedat/core/routes";
10
11
  import { getActiveTab } from "../selectors/getActiveTab";
@@ -22,6 +23,7 @@ import StructureVersions from "./StructureVersions";
22
23
  import StructureMetadata from "./StructureMetadata";
23
24
  import StructureNotesLoader from "./StructureNotesLoader";
24
25
  import StructureStructureLinks from "./StructureStructureLinks";
26
+ import StructureStructureForm from "./StructureStructureForm";
25
27
 
26
28
  const StructureLinks = React.lazy(() =>
27
29
  import("@truedat/lm/components/StructureLinks")
@@ -63,7 +65,15 @@ export const StructureTabPane = ({ activeTab, tabVisibility }) => {
63
65
  <Route render={() => <StructureLinks />} />
64
66
  </Switch>
65
67
  )}
66
- {activeTab === "structureLinks" && <StructureStructureLinks />}
68
+ {activeTab === "structureLinks" && (
69
+ <Switch>
70
+ <Route
71
+ path={STRUCTURE_STRUCTURE_LINKS_NEW}
72
+ render={() => <StructureStructureForm />}
73
+ />
74
+ <Route render={() => <StructureStructureLinks />} />
75
+ </Switch>
76
+ )}
67
77
  {activeTab === "profile" && tabVisibility.profile && <StructureProfile />}
68
78
  {activeTab === "parents" && <StructureParentRelations />}
69
79
  {activeTab === "children" && <StructureChildrenRelations />}
@@ -7,7 +7,7 @@ import {
7
7
  STRUCTURE_GRANTS,
8
8
  STRUCTURE_IMPACT,
9
9
  STRUCTURE_LINEAGE,
10
- STRUCTURE_LINKED_STRUCTURES,
10
+ STRUCTURE_STRUCTURE_LINKS,
11
11
  STRUCTURE_LINKS,
12
12
  STRUCTURE_METADATA,
13
13
  STRUCTURE_NOTES,
@@ -25,7 +25,7 @@ export const StructureTabPaneRoutes = ({}) => (
25
25
  <Route path={STRUCTURE_VERSION_VERSIONS} component={StructureTabPane} />
26
26
  <Route path={STRUCTURE_VERSION} component={StructureTabPane} />
27
27
  <Route path={STRUCTURE_PROFILE} component={StructureTabPane} />
28
- <Route path={STRUCTURE_LINKED_STRUCTURES} component={StructureTabPane} />
28
+ <Route path={STRUCTURE_STRUCTURE_LINKS} component={StructureTabPane} />
29
29
  <Route path={STRUCTURE_LINKS} component={StructureTabPane} />
30
30
  <Route path={STRUCTURE_VERSIONS} component={StructureTabPane} />
31
31
  <Route path={STRUCTURE_EVENTS} component={StructureTabPane} />
@@ -8,7 +8,7 @@ import {
8
8
  STRUCTURE_IMPACT,
9
9
  STRUCTURE_LINEAGE,
10
10
  STRUCTURE_LINKS,
11
- STRUCTURE_LINKED_STRUCTURES,
11
+ STRUCTURE_STRUCTURE_LINKS,
12
12
  STRUCTURE_NOTES,
13
13
  STRUCTURE_PARENTS,
14
14
  STRUCTURE_PROFILE,
@@ -26,7 +26,7 @@ export const StructureTabRoutes = () => (
26
26
  <Route path={STRUCTURE_VERSION} component={StructureTabs} />
27
27
  <Route path={STRUCTURE_PROFILE} component={StructureTabs} />
28
28
  <Route path={STRUCTURE_LINKS} component={StructureTabs} />
29
- <Route path={STRUCTURE_LINKED_STRUCTURES} component={StructureTabs} />
29
+ <Route path={STRUCTURE_STRUCTURE_LINKS} component={StructureTabs} />
30
30
  <Route path={STRUCTURE_VERSIONS} component={StructureTabs} />
31
31
  <Route path={STRUCTURE_EVENTS} component={StructureTabs} />
32
32
  <Route path={STRUCTURE_METADATA} component={StructureTabs} />
@@ -109,7 +109,7 @@ export const StructureTabs = ({
109
109
  {
110
110
  active: activeTab === "structureLinks",
111
111
  as: Link,
112
- to: linkTo.STRUCTURE_LINKED_STRUCTURES(structure),
112
+ to: linkTo.STRUCTURE_STRUCTURE_LINKS(structure),
113
113
  content: structureLinks
114
114
  ? formatMessage({ id: "tabs.dd.structureLinks" })
115
115
  : false,
@@ -0,0 +1,138 @@
1
+ import React from "react";
2
+ import { waitFor } from "@testing-library/react";
3
+ import { render } from "@truedat/test/render";
4
+ import userEvent from "@testing-library/user-event";
5
+ import StructureStructureForm from "../StructureStructureForm";
6
+ import { linkStructureToStructure } from "../../routines";
7
+ import { LINK_LABELS_QUERY } from "../../api/queries";
8
+
9
+ const linkLabels = {
10
+ labels: [
11
+ {
12
+ id: "1",
13
+ name: "is_copy_from",
14
+ },
15
+ {
16
+ id: "2",
17
+ name: "reimu_ga_tsukutta",
18
+ },
19
+ {
20
+ id: "3",
21
+ name: "marisa_ga_tsukutta",
22
+ },
23
+ ],
24
+ };
25
+
26
+ const linkLabelsMock = {
27
+ request: { query: LINK_LABELS_QUERY },
28
+ result: { data: linkLabels },
29
+ };
30
+
31
+ describe("<StructureStructureForm />", () => {
32
+ const structure = {
33
+ alias: null,
34
+ confidential: false,
35
+ domain_ids: [1],
36
+ domains: [
37
+ {
38
+ external_id: "Acme",
39
+ id: 1,
40
+ name: "Acme",
41
+ parent_id: null,
42
+ parents: [],
43
+ updated_at: "2022-03-22 12:56:39.639127Z",
44
+ },
45
+ ],
46
+ external_id: "external_id",
47
+ id: 67583,
48
+ inserted_at: "2019-11-27T08:35:44.000000Z",
49
+ updated_at: "2021-06-10T12:01:17.343065Z",
50
+ class: null,
51
+ classes: null,
52
+ data_structure_link_count: 0,
53
+ data_structure_type: {
54
+ metadata_views: [],
55
+ template_id: 9,
56
+ translation: null,
57
+ },
58
+ degree: null,
59
+ deleted_at: null,
60
+ description: null,
61
+ grant: null,
62
+ grants: [],
63
+ group: "group",
64
+ implementation_count: 0,
65
+ metadata: {
66
+ database: "database",
67
+ host: "host",
68
+ },
69
+ name: "structure",
70
+ note: {},
71
+ siblings: [],
72
+ source: {
73
+ external_id: "oracle_meta_ds2",
74
+ id: 19,
75
+ },
76
+ system: {
77
+ id: 2,
78
+ name: "SYSTEM_1",
79
+ },
80
+ type: "Schema",
81
+ };
82
+
83
+ it("matches the latest snapshot", async () => {
84
+ const columns = ["name", "system", "group"].map((name) => ({ name }));
85
+ const structureActiveFilters = {};
86
+ const structuresRows = [{ id: 1 }, { id: 2 }];
87
+ const dispatch = jest.fn();
88
+
89
+ const props = {
90
+ linkStructureToStructure,
91
+ structuresRows,
92
+ columns,
93
+ onSelect: jest.fn(),
94
+ };
95
+
96
+ const renderOpts = {
97
+ mocks: [linkLabelsMock],
98
+ state: {
99
+ structure,
100
+ structures: [structure],
101
+ structureActiveFilters,
102
+ },
103
+ dispatch,
104
+ };
105
+
106
+ const { queryByText, findByText, getByRole } = render(
107
+ <StructureStructureForm {...props} />,
108
+ renderOpts
109
+ );
110
+ await waitFor(() =>
111
+ expect(queryByText(/loading/i)).not.toBeInTheDocument()
112
+ );
113
+ userEvent.click(await findByText("structure"));
114
+
115
+ const saveButton = getByRole("button", { name: "Save" });
116
+ await waitFor(() => {
117
+ expect(saveButton).toBeEnabled();
118
+ });
119
+
120
+ const label1 = getByRole("option", { name: "is_copy_from" });
121
+ const label2 = getByRole("option", { name: "reimu_ga_tsukutta" });
122
+
123
+ userEvent.click(label1);
124
+ userEvent.click(label2);
125
+
126
+ userEvent.click(saveButton);
127
+ await new Promise((r) => setTimeout(r, 200));
128
+
129
+ expect(dispatch).toHaveBeenCalledWith({
130
+ ...linkStructureToStructure(),
131
+ payload: {
132
+ sourceStructureId: 67583,
133
+ targetStructureId: 67583,
134
+ labelIds: ["1", "2"],
135
+ },
136
+ });
137
+ });
138
+ });