@truedat/dd 7.12.1 → 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 +3 -3
- package/src/api.js +3 -0
- package/src/components/StructureNotesDownloadButton.js +78 -0
- package/src/components/StructureSummary.js +5 -1
- package/src/components/StructuresView.js +1 -1
- package/src/components/__tests__/StructureNotesDownloadButton.spec.js +219 -0
- package/src/components/__tests__/__snapshots__/StructureSummary.spec.js.snap +16 -0
- package/src/hooks/useStructures.js +14 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@truedat/dd",
|
|
3
|
-
"version": "7.12.
|
|
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.
|
|
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": "
|
|
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
|
};
|
|
@@ -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);
|
|
@@ -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
|
+
};
|