@truedat/dd 7.12.0 → 7.12.2

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": "7.12.0",
3
+ "version": "7.12.2",
4
4
  "description": "Truedat Web Data Dictionary",
5
5
  "sideEffects": false,
6
6
  "module": "src/index.js",
@@ -48,7 +48,7 @@
48
48
  "@testing-library/jest-dom": "^6.6.3",
49
49
  "@testing-library/react": "^16.3.0",
50
50
  "@testing-library/user-event": "^14.6.1",
51
- "@truedat/test": "7.12.0",
51
+ "@truedat/test": "7.12.2",
52
52
  "identity-obj-proxy": "^3.0.0",
53
53
  "jest": "^29.7.0",
54
54
  "redux-saga-test-plan": "^4.0.6"
@@ -83,5 +83,5 @@
83
83
  "svg-pan-zoom": "^3.6.2",
84
84
  "swr": "^2.3.3"
85
85
  },
86
- "gitHead": "7f3f823a6cafd90fa9a326e950fe7b8bcf966b70"
86
+ "gitHead": "b5edde19955e90a0e17d4e8bdaf2c1fe871bc3b9"
87
87
  }
package/src/api.js CHANGED
@@ -53,6 +53,8 @@ const API_STRUCTURES_UPLOAD_EVENTS =
53
53
  "/api/data_structures/bulk_update_template_content_events";
54
54
  const API_STRUCTURE_NOTE = "/api/data_structures/:data_structure_id/notes/:id";
55
55
  const API_STRUCTURE_NOTES = "/api/data_structures/:data_structure_id/notes";
56
+ const API_STRUCTURE_NOTES_XLSX_DOWNLOAD =
57
+ "/api/data_structures/:id/notes/xlsx/download";
56
58
 
57
59
  export {
58
60
  API_BUCKET_STRUCTURES,
@@ -104,6 +106,7 @@ export {
104
106
  API_STRUCTURES_UPLOAD_EVENTS,
105
107
  API_STRUCTURE_NOTE,
106
108
  API_STRUCTURE_NOTES,
109
+ API_STRUCTURE_NOTES_XLSX_DOWNLOAD,
107
110
  API_STRUCTURE_TO_STRUCTURE_LINKS,
108
111
  API_STRUCTURE_TO_STRUCTURE_LINK,
109
112
  };
@@ -28,9 +28,8 @@ export const EditDomainForm = ({
28
28
 
29
29
  const onSubmit = () => {
30
30
  const data_structure = { id: structure.id, domain_ids: domainIds };
31
- const redirect = linkTo.STRUCTURE_NOTES(data_structure);
32
31
 
33
- updateStructure({ data_structure, inherit, redirect });
32
+ updateStructure({ data_structure, inherit });
34
33
  };
35
34
 
36
35
  return (
@@ -0,0 +1,78 @@
1
+ import { useState } from "react";
2
+ import PropTypes from "prop-types";
3
+ import { Button, Checkbox, Modal, Icon } from "semantic-ui-react";
4
+ import { useIntl } from "react-intl";
5
+ import { useStructureNotesDownload } from "../hooks/useStructures";
6
+
7
+ export const StructureNotesDownloadButton = ({ structureId }) => {
8
+ const { formatMessage, locale } = useIntl();
9
+ const [open, setOpen] = useState(false);
10
+ const [selectedStatuses, setSelectedStatuses] = useState(["published"]);
11
+ const [includeChildren, setIncludeChildren] = useState(false);
12
+
13
+ const { trigger: triggerDownload, isMutating: isDownloading } =
14
+ useStructureNotesDownload();
15
+
16
+ const handleDownload = () => {
17
+ triggerDownload({
18
+ id: structureId,
19
+ statuses: selectedStatuses,
20
+ include_children: includeChildren,
21
+ lang: locale
22
+ });
23
+ setOpen(false);
24
+ };
25
+
26
+ return (
27
+ <Modal
28
+ trigger={
29
+ <Button
30
+ icon="download"
31
+ onClick={() => setOpen(true)}
32
+ loading={isDownloading}
33
+ className="basic structureButton profile"
34
+ />
35
+ }
36
+ open={open}
37
+ onClose={() => setOpen(false)}
38
+ size="small"
39
+ >
40
+ <Modal.Header>
41
+ <Icon name="download" />
42
+ {formatMessage({ id: "structure.actions.downloadNotes.modal.title" })}
43
+ </Modal.Header>
44
+ <Modal.Content>
45
+ <Checkbox
46
+ className="bgOrange"
47
+ toggle
48
+ label={formatMessage({
49
+ id: "structure.actions.downloadNotes.includeChildren",
50
+ })}
51
+ onChange={() => setIncludeChildren(!includeChildren)}
52
+ checked={includeChildren}
53
+ style={{ top: "6px", marginRight: "7.5px" }}
54
+ />
55
+
56
+ </Modal.Content>
57
+ <Modal.Actions>
58
+ <Button onClick={() => setOpen(false)}>
59
+ {formatMessage({ id: "actions.cancel" })}
60
+ </Button>
61
+ <Button
62
+ primary
63
+ onClick={handleDownload}
64
+ loading={isDownloading}
65
+ >
66
+ <Icon name="download" />
67
+ {formatMessage({ id: "actions.download" })}
68
+ </Button>
69
+ </Modal.Actions>
70
+ </Modal>
71
+ );
72
+ };
73
+
74
+ StructureNotesDownloadButton.propTypes = {
75
+ structureId: PropTypes.string.isRequired,
76
+ };
77
+
78
+ export default StructureNotesDownloadButton;
@@ -10,6 +10,7 @@ import StructureProperties from "./StructureProperties";
10
10
  import StructureConfidentialButton from "./StructureConfidentialButton";
11
11
  import StructureDeleteButton from "./StructureDeleteButton";
12
12
  import StructureGrantSummaryButton from "./StructureGrantSummaryButton";
13
+ import StructureNotesDownloadButton from "./StructureNotesDownloadButton";
13
14
  import StructureProfileButton from "./StructureProfileButton";
14
15
  import StructureTags from "./StructureTags";
15
16
 
@@ -50,6 +51,7 @@ const dateProperties = (deleted_at, updatedAt) =>
50
51
  };
51
52
 
52
53
  export const StructureSummary = ({
54
+ id,
53
55
  class: structureClass,
54
56
  classes: structureClasses,
55
57
  name,
@@ -63,7 +65,7 @@ export const StructureSummary = ({
63
65
  const { formatMessage } = useIntl();
64
66
  const shareUrl = window.location.href;
65
67
  const shareTitle = formatMessage({ id: "structure.share.title" });
66
-
68
+
67
69
  return (
68
70
  <>
69
71
  <Grid>
@@ -94,6 +96,7 @@ export const StructureSummary = ({
94
96
  icon="structures"
95
97
  popupType="structures"
96
98
  />
99
+ <StructureNotesDownloadButton structureId={id} />
97
100
  {userPermissions.profile_permission &&
98
101
  structureClass === "field" ? (
99
102
  <StructureProfileButton />
@@ -112,6 +115,7 @@ export const StructureSummary = ({
112
115
  };
113
116
 
114
117
  StructureSummary.propTypes = {
118
+ id: PropTypes.string,
115
119
  class: PropTypes.string,
116
120
  classes: PropTypes.object,
117
121
  name: PropTypes.string,
@@ -19,7 +19,7 @@ import StructuresOptions from "./StructuresOptions";
19
19
  import StructuresSearchResults from "./StructuresSearchResults";
20
20
  import SystemCards from "./SystemCards";
21
21
  import CatalogCustomViewCards from "./CatalogCustomViewCards";
22
-
22
+
23
23
  const StructuresHeader = () => {
24
24
  const { formatMessage } = useIntl();
25
25
  const match = useMatch(BUCKETS_VIEW);
@@ -70,7 +70,6 @@ describe("<EditDomainForm />", () => {
70
70
  expect.objectContaining({
71
71
  data_structure: { domain_ids: ["1"], id: 42 },
72
72
  inherit: true,
73
- redirect: "/structures/42/notes",
74
73
  })
75
74
  );
76
75
  });
@@ -97,7 +96,6 @@ describe("<EditDomainForm />", () => {
97
96
  expect.objectContaining({
98
97
  data_structure: { domain_ids: ["1"], id: 42 },
99
98
  inherit: false,
100
- redirect: "/structures/42/notes",
101
99
  })
102
100
  );
103
101
  });
@@ -124,7 +122,6 @@ describe("<EditDomainForm />", () => {
124
122
  expect.objectContaining({
125
123
  data_structure: { domain_ids: ["2", "1"], id: 42 },
126
124
  inherit: true,
127
- redirect: "/structures/42/notes",
128
125
  })
129
126
  );
130
127
  });
@@ -0,0 +1,219 @@
1
+ import React from "react";
2
+ import { render, screen, fireEvent, waitFor } from "@testing-library/react";
3
+ import { IntlProvider } from "react-intl";
4
+ import { StructureNotesDownloadButton } from "../StructureNotesDownloadButton";
5
+ import * as useStructures from "../../hooks/useStructures";
6
+
7
+ const messages = {
8
+ "structure.actions.downloadNotes": "Download notes",
9
+ "structure.actions.downloadNotes.modal.title": "Download Structure Notes",
10
+ "structure.actions.downloadNotes.includeChildren": "Include direct children notes",
11
+ "actions.cancel": "Cancel",
12
+ "actions.download": "Download",
13
+ };
14
+
15
+ const renderWithIntl = (component) => {
16
+ return render(
17
+ <IntlProvider locale="en" messages={messages}>
18
+ {component}
19
+ </IntlProvider>
20
+ );
21
+ };
22
+
23
+ // Helper function to get checkbox by label text
24
+ const getCheckboxByLabel = (labelText) => {
25
+ const label = screen.getByText(labelText);
26
+ const checkboxContainer = label.closest(".ui.checkbox");
27
+ return checkboxContainer.querySelector("input[type='checkbox']");
28
+ };
29
+
30
+ describe("StructureNotesDownloadButton", () => {
31
+ const mockTrigger = jest.fn();
32
+ const mockStructureId = "123";
33
+
34
+ beforeEach(() => {
35
+ jest.clearAllMocks();
36
+ jest.spyOn(useStructures, "useStructureNotesDownload").mockReturnValue({
37
+ trigger: mockTrigger,
38
+ isMutating: false,
39
+ });
40
+ });
41
+
42
+ it("always renders download button", () => {
43
+ renderWithIntl(
44
+ <StructureNotesDownloadButton
45
+ structureId={mockStructureId}
46
+ />
47
+ );
48
+
49
+ const button = screen.getByRole("button");
50
+ expect(button).toBeInTheDocument();
51
+ expect(button).toHaveAttribute("class", expect.stringContaining("basic"));
52
+ });
53
+
54
+ it("opens modal when button is clicked", () => {
55
+ renderWithIntl(
56
+ <StructureNotesDownloadButton
57
+ structureId={mockStructureId}
58
+ />
59
+ );
60
+
61
+ const button = screen.getByRole("button");
62
+ fireEvent.click(button);
63
+
64
+ expect(
65
+ screen.getByText("Download Structure Notes")
66
+ ).toBeInTheDocument();
67
+ });
68
+
69
+ it("displays include children checkbox in modal", () => {
70
+ renderWithIntl(
71
+ <StructureNotesDownloadButton
72
+ structureId={mockStructureId}
73
+ />
74
+ );
75
+
76
+ const button = screen.getByRole("button");
77
+ fireEvent.click(button);
78
+
79
+ const includeChildrenCheckbox = screen.getByText("Include direct children notes");
80
+ expect(includeChildrenCheckbox).toBeInTheDocument();
81
+ });
82
+
83
+ it("allows toggling include children checkbox", () => {
84
+ renderWithIntl(
85
+ <StructureNotesDownloadButton
86
+ structureId={mockStructureId}
87
+ />
88
+ );
89
+
90
+ const button = screen.getByRole("button");
91
+ fireEvent.click(button);
92
+
93
+ const includeChildrenCheckbox = getCheckboxByLabel("Include direct children notes");
94
+
95
+ // Initially should not be checked
96
+ expect(includeChildrenCheckbox).not.toBeChecked();
97
+
98
+ fireEvent.click(includeChildrenCheckbox);
99
+ expect(includeChildrenCheckbox).toBeChecked();
100
+
101
+ fireEvent.click(includeChildrenCheckbox);
102
+ expect(includeChildrenCheckbox).not.toBeChecked();
103
+ });
104
+
105
+ it("calls trigger with correct parameters when download button is clicked", async () => {
106
+ renderWithIntl(
107
+ <StructureNotesDownloadButton
108
+ structureId={mockStructureId}
109
+ />
110
+ );
111
+
112
+ const openButton = screen.getByRole("button");
113
+ fireEvent.click(openButton);
114
+
115
+ // Toggle the "Include direct children notes" checkbox to checked
116
+ const includeChildrenCheckbox = getCheckboxByLabel("Include direct children notes");
117
+ fireEvent.click(includeChildrenCheckbox);
118
+ expect(includeChildrenCheckbox).toBeChecked();
119
+
120
+ const downloadButton = screen.getByRole("button", { name: /^download$/i });
121
+ fireEvent.click(downloadButton);
122
+
123
+ await waitFor(() => {
124
+ expect(mockTrigger).toHaveBeenCalledWith({
125
+ id: mockStructureId,
126
+ include_children: true,
127
+ lang: "en",
128
+ statuses: ["published"]
129
+ });
130
+ });
131
+ });
132
+
133
+ it("calls trigger with include_children false when checkbox is not selected", async () => {
134
+ renderWithIntl(
135
+ <StructureNotesDownloadButton
136
+ structureId={mockStructureId}
137
+ />
138
+ );
139
+
140
+ const openButton = screen.getByRole("button");
141
+ fireEvent.click(openButton);
142
+
143
+ // Ensure "Include direct children notes" checkbox is not checked
144
+ const includeChildrenCheckbox = getCheckboxByLabel("Include direct children notes");
145
+ expect(includeChildrenCheckbox).not.toBeChecked();
146
+
147
+ const downloadButton = screen.getByRole("button", { name: /^download$/i });
148
+ fireEvent.click(downloadButton);
149
+
150
+ await waitFor(() => {
151
+ expect(mockTrigger).toHaveBeenCalledWith({
152
+ id: mockStructureId,
153
+ include_children: false,
154
+ lang: "en",
155
+ statuses: ["published"]
156
+ });
157
+ });
158
+ });
159
+
160
+ it("closes modal after download", async () => {
161
+ renderWithIntl(
162
+ <StructureNotesDownloadButton
163
+ structureId={mockStructureId}
164
+ />
165
+ );
166
+
167
+ const openButton = screen.getByRole("button");
168
+ fireEvent.click(openButton);
169
+
170
+ const modalTitle = screen.getByText("Download Structure Notes");
171
+ expect(modalTitle).toBeInTheDocument();
172
+
173
+ const downloadButton = screen.getByRole("button", { name: /^download$/i });
174
+ fireEvent.click(downloadButton);
175
+
176
+ await waitFor(() => {
177
+ expect(
178
+ screen.queryByText("Download Structure Notes")
179
+ ).not.toBeInTheDocument();
180
+ });
181
+ });
182
+
183
+ it("shows loading state when downloading", () => {
184
+ jest.spyOn(useStructures, "useStructureNotesDownload").mockReturnValue({
185
+ trigger: mockTrigger,
186
+ isMutating: true,
187
+ });
188
+
189
+ renderWithIntl(
190
+ <StructureNotesDownloadButton
191
+ structureId={mockStructureId}
192
+ />
193
+ );
194
+
195
+ const button = screen.getByRole("button");
196
+ expect(button).toHaveAttribute("class", expect.stringContaining("loading"));
197
+ });
198
+
199
+ it("closes modal when cancel button is clicked", () => {
200
+ renderWithIntl(
201
+ <StructureNotesDownloadButton
202
+ structureId={mockStructureId}
203
+ />
204
+ );
205
+
206
+ const openButton = screen.getByRole("button");
207
+ fireEvent.click(openButton);
208
+
209
+ const modalTitle = screen.getByText("Download Structure Notes");
210
+ expect(modalTitle).toBeInTheDocument();
211
+
212
+ const cancelButton = screen.getByRole("button", { name: /cancel/i });
213
+ fireEvent.click(cancelButton);
214
+
215
+ expect(
216
+ screen.queryByText("Download Structure Notes")
217
+ ).not.toBeInTheDocument();
218
+ });
219
+ });
@@ -52,6 +52,14 @@ exports[`<StructureSummary /> matches the latest snapshot 1`] = `
52
52
  class="share alternate icon"
53
53
  />
54
54
  </button>
55
+ <button
56
+ class="ui icon button basic structureButton profile"
57
+ >
58
+ <i
59
+ aria-hidden="true"
60
+ class="download icon"
61
+ />
62
+ </button>
55
63
  <button
56
64
  class="ui icon button basic"
57
65
  >
@@ -200,6 +208,14 @@ exports[`<StructureSummary /> matches the latest snapshot with alias 1`] = `
200
208
  class="share alternate icon"
201
209
  />
202
210
  </button>
211
+ <button
212
+ class="ui icon button basic structureButton profile"
213
+ >
214
+ <i
215
+ aria-hidden="true"
216
+ class="download icon"
217
+ />
218
+ </button>
203
219
  <button
204
220
  class="ui icon button basic"
205
221
  >
@@ -16,6 +16,7 @@ import {
16
16
  API_DATA_STRUCTURE_FILTERS_SEARCH,
17
17
  API_DATA_STRUCTURES_XLSX_DOWNLOAD,
18
18
  API_DATA_STRUCTURES_XLSX_UPLOAD,
19
+ API_STRUCTURE_NOTES_XLSX_DOWNLOAD,
19
20
  } from "../api";
20
21
 
21
22
  const toApiPath = compile(API_SYSTEM_STRUCTURES);
@@ -95,3 +96,16 @@ export const useDataStructureSuggestions = () => {
95
96
  return apiJsonPost(url, arg);
96
97
  });
97
98
  };
99
+
100
+ export const useStructureNotesDownload = () => {
101
+ return useSWRMutations(API_STRUCTURE_NOTES_XLSX_DOWNLOAD, (url, { arg }) => {
102
+ const { id, ...params } = arg;
103
+ const downloadUrl = url.replace(":id", id);
104
+ return apiJsonPost(downloadUrl, params, {
105
+ ...JSON_OPTS,
106
+ responseType: "blob",
107
+ }).then(({ data, headers }) => {
108
+ saveFile({ data, headers });
109
+ });
110
+ });
111
+ };
@@ -5,7 +5,7 @@ import {
5
5
  updateStructureRequestSaga,
6
6
  updateStructureSaga,
7
7
  } from "../updateStructure";
8
- import { updateStructure } from "../../routines";
8
+ import { updateStructure, fetchStructure} from "../../routines";
9
9
  import { API_DATA_STRUCTURE } from "../../api";
10
10
 
11
11
  describe("sagas: updateStructureRequestSaga", () => {
@@ -50,6 +50,8 @@ describe("sagas: updateStructureSaga", () => {
50
50
  .next({ data: data_structure })
51
51
  .put(updateStructure.success(successData))
52
52
  .next()
53
+ .put(fetchStructure.trigger(data_structure))
54
+ .next()
53
55
  .put(updateStructure.fulfill())
54
56
  .next()
55
57
  .isDone();
@@ -2,7 +2,8 @@ import _ from "lodash/fp";
2
2
  import { compile } from "path-to-regexp";
3
3
  import { call, put, takeLatest } from "redux-saga/effects";
4
4
  import { apiJsonPatch, JSON_OPTS } from "@truedat/core/services/api";
5
- import { updateStructure } from "../routines";
5
+ import { updateStructure, fetchStructure } from "../routines";
6
+
6
7
  import { API_DATA_STRUCTURE } from "../api";
7
8
 
8
9
  export function* updateStructureSaga({ payload }) {
@@ -15,8 +16,10 @@ export function* updateStructureSaga({ payload }) {
15
16
  };
16
17
  yield put(updateStructure.request());
17
18
  const { data } = yield call(apiJsonPatch, url, requestData, JSON_OPTS);
19
+
18
20
  const successPayload = redirect ? { ...data, redirect } : data;
19
21
  yield put(updateStructure.success(successPayload));
22
+ yield put(fetchStructure.trigger(data_structure));
20
23
  } catch (error) {
21
24
  if (error.response) {
22
25
  const { status, data } = error.response;