Skip to content

Commit 72b8818

Browse files
authored
Merge pull request #17507 from guerler/remove_copy_dataset_mako
Replace Copy Dataset Mako with Vue Component
2 parents 426fcd1 + 174e2fa commit 72b8818

File tree

18 files changed

+780
-360
lines changed

18 files changed

+780
-360
lines changed

client/src/api/schema/schema.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2421,6 +2421,23 @@ export interface paths {
24212421
patch?: never;
24222422
trace?: never;
24232423
};
2424+
"/api/histories/{history_id}/copy_contents": {
2425+
parameters: {
2426+
query?: never;
2427+
header?: never;
2428+
path?: never;
2429+
cookie?: never;
2430+
};
2431+
get?: never;
2432+
put?: never;
2433+
/** Copy datasets or dataset collections to other histories. */
2434+
post: operations["history_contents__copy_contents"];
2435+
delete?: never;
2436+
options?: never;
2437+
head?: never;
2438+
patch?: never;
2439+
trace?: never;
2440+
};
24242441
"/api/histories/{history_id}/custom_builds_metadata": {
24252442
parameters: {
24262443
query?: never;
@@ -8206,6 +8223,27 @@ export interface components {
82068223
ConvertedDatasetsMap: {
82078224
[key: string]: string;
82088225
};
8226+
/** CopyDatasetsPayload */
8227+
CopyDatasetsPayload: {
8228+
/** Source Content */
8229+
source_content: components["schemas"]["CopyDatasetsPayloadSourceEntry"][];
8230+
/** Target History Ids */
8231+
target_history_ids?: string[] | null;
8232+
/** Target History Name */
8233+
target_history_name?: string | null;
8234+
};
8235+
/** CopyDatasetsPayloadSourceEntry */
8236+
CopyDatasetsPayloadSourceEntry: {
8237+
/** Id */
8238+
id: string;
8239+
/** Type */
8240+
type: string;
8241+
};
8242+
/** CopyDatasetsResponse */
8243+
CopyDatasetsResponse: {
8244+
/** History Ids */
8245+
history_ids: string[];
8246+
};
82098247
/** CreateDataLandingPayload */
82108248
CreateDataLandingPayload: {
82118249
/** Client Secret */
@@ -32469,6 +32507,54 @@ export interface operations {
3246932507
};
3247032508
};
3247132509
};
32510+
history_contents__copy_contents: {
32511+
parameters: {
32512+
query?: never;
32513+
header?: {
32514+
/** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */
32515+
"run-as"?: string | null;
32516+
};
32517+
path: {
32518+
/** @description The encoded database identifier of the History. */
32519+
history_id: string;
32520+
};
32521+
cookie?: never;
32522+
};
32523+
requestBody: {
32524+
content: {
32525+
"application/json": components["schemas"]["CopyDatasetsPayload"];
32526+
};
32527+
};
32528+
responses: {
32529+
/** @description Successful Response */
32530+
200: {
32531+
headers: {
32532+
[name: string]: unknown;
32533+
};
32534+
content: {
32535+
"application/json": components["schemas"]["CopyDatasetsResponse"];
32536+
};
32537+
};
32538+
/** @description Request Error */
32539+
"4XX": {
32540+
headers: {
32541+
[name: string]: unknown;
32542+
};
32543+
content: {
32544+
"application/json": components["schemas"]["MessageExceptionModel"];
32545+
};
32546+
};
32547+
/** @description Server Error */
32548+
"5XX": {
32549+
headers: {
32550+
[name: string]: unknown;
32551+
};
32552+
content: {
32553+
"application/json": components["schemas"]["MessageExceptionModel"];
32554+
};
32555+
};
32556+
};
32557+
};
3247232558
get_custom_builds_metadata_api_histories__history_id__custom_builds_metadata_get: {
3247332559
parameters: {
3247432560
query?: never;
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
import { getLocalVue } from "@tests/vitest/helpers";
2+
import { mount } from "@vue/test-utils";
3+
import flushPromises from "flush-promises";
4+
import { createPinia } from "pinia";
5+
import { beforeEach, expect, it } from "vitest";
6+
7+
import { useServerMock } from "@/api/client/__mocks__";
8+
import { useHistoryStore } from "@/stores/historyStore";
9+
10+
import DatasetCopy from "./DatasetCopy.vue";
11+
12+
const { server, http } = useServerMock();
13+
const localVue = getLocalVue();
14+
const pinia = createPinia();
15+
16+
beforeEach(() => {
17+
server.resetHandlers();
18+
});
19+
20+
function mountComponent() {
21+
const wrapper = mount(DatasetCopy, {
22+
localVue,
23+
directives: { localize: () => {} },
24+
stubs: { RouterLink: { template: "<a><slot /></a>" } },
25+
pinia,
26+
});
27+
const historyStore = useHistoryStore();
28+
historyStore.setCurrentHistoryId("h1");
29+
return wrapper;
30+
}
31+
32+
async function setupBase(histories, contents) {
33+
server.use(
34+
http.get("/api/histories", ({ response }) => response(200).json(histories)),
35+
http.get("/api/histories/{history_id}", ({ params, response }) =>
36+
response(200).json({ id: params.history_id, name: "H1" }),
37+
),
38+
http.get("/api/histories/{history_id}/contents", ({ response }) => response(200).json(contents)),
39+
);
40+
const wrapper = mountComponent();
41+
await flushPromises();
42+
const checkbox = wrapper.find("input[type='checkbox']");
43+
await checkbox.setChecked(true);
44+
return wrapper;
45+
}
46+
47+
it("loads histories and contents on mount", async () => {
48+
server.use(
49+
http.get("/api/histories", ({ response }) =>
50+
response(200).json([
51+
{ id: "h1", name: "History One" },
52+
{ id: "h2", name: "History Two" },
53+
]),
54+
),
55+
http.get("/api/histories/{history_id}", ({ params, response }) =>
56+
response(200).json({ id: params.history_id, name: `History ${params.history_id}` }),
57+
),
58+
http.get("/api/histories/{history_id}/contents", ({ response }) =>
59+
response(200).json([
60+
{ id: "d1", name: "A", hid: 1, history_content_type: "dataset" },
61+
{ id: "d2", name: "B", hid: 2, history_content_type: "collection" },
62+
]),
63+
),
64+
);
65+
const wrapper = mountComponent();
66+
await flushPromises();
67+
expect(wrapper.text()).toContain("History One");
68+
expect(wrapper.text()).toContain("History Two");
69+
expect(wrapper.text()).toContain("A");
70+
expect(wrapper.text()).toContain("B");
71+
});
72+
73+
it("copies selected items and shows success", async () => {
74+
server.use(
75+
http.get("/api/histories", ({ response }) => response(200).json([{ id: "h1", name: "H1" }])),
76+
http.get("/api/histories/{history_id}", ({ params, response }) =>
77+
response(200).json({ id: params.history_id, name: "H1" }),
78+
),
79+
http.get("/api/histories/{history_id}/contents", ({ response }) =>
80+
response(200).json([{ id: "d1", name: "X", hid: 1, history_content_type: "dataset" }]),
81+
),
82+
http.post("/api/histories/{history_id}/copy_contents", async ({ request, response }) => {
83+
const body = await request.json();
84+
if (body.source_content.length > 0) {
85+
return response(200).json({ history_ids: ["h1"] });
86+
}
87+
return response(400).json({ err_msg: "No data" });
88+
}),
89+
);
90+
const wrapper = mountComponent();
91+
await flushPromises();
92+
const checkbox = wrapper.find("input[type='checkbox']");
93+
await checkbox.setChecked(true);
94+
await wrapper.find("button.btn-primary").trigger("click");
95+
await flushPromises();
96+
expect(wrapper.text()).toMatch(/1 item[s]? copied/);
97+
});
98+
99+
it("shows error when nothing selected", async () => {
100+
server.use(
101+
http.get("/api/histories", ({ response }) => response(200).json([{ id: "h1", name: "H1" }])),
102+
http.get("/api/histories/{history_id}", ({ params, response }) =>
103+
response(200).json({ id: params.history_id, name: "H1" }),
104+
),
105+
http.get("/api/histories/{history_id}/contents", ({ response }) => response(200).json([])),
106+
);
107+
const wrapper = mountComponent();
108+
await flushPromises();
109+
await wrapper.find("button.btn-primary").trigger("click");
110+
await flushPromises();
111+
expect(wrapper.text()).toContain("Please select datasets and collections.");
112+
});
113+
114+
it("handles API error from copy call", async () => {
115+
server.use(
116+
http.get("/api/histories", ({ response }) => response(200).json([{ id: "h1", name: "H1" }])),
117+
http.get("/api/histories/{history_id}", ({ params, response }) =>
118+
response(200).json({ id: params.history_id, name: "H1" }),
119+
),
120+
http.get("/api/histories/{history_id}/contents", ({ response }) =>
121+
response(200).json([{ id: "d1", name: "X", hid: 1, history_content_type: "dataset" }]),
122+
),
123+
http.post("/api/histories/{history_id}/copy_contents", ({ response }) =>
124+
response(500).json({ err_msg: "Copy failed" }),
125+
),
126+
);
127+
const wrapper = mountComponent();
128+
await flushPromises();
129+
const checkbox = wrapper.find("input[type='checkbox']");
130+
await checkbox.setChecked(true);
131+
await wrapper.find("button.btn-primary").trigger("click");
132+
await flushPromises();
133+
expect(wrapper.text()).toContain("Copy failed");
134+
});
135+
136+
it("toggleAll selects and unselects all", async () => {
137+
server.use(
138+
http.get("/api/histories", ({ response }) => response(200).json([{ id: "h1", name: "H1" }])),
139+
http.get("/api/histories/{history_id}", ({ params, response }) =>
140+
response(200).json({ id: params.history_id, name: "H1" }),
141+
),
142+
http.get("/api/histories/{history_id}/contents", ({ response }) =>
143+
response(200).json([
144+
{ id: "d1", name: "X", hid: 1, history_content_type: "dataset" },
145+
{ id: "d2", name: "Y", hid: 2, history_content_type: "dataset" },
146+
]),
147+
),
148+
);
149+
const wrapper = mountComponent();
150+
await flushPromises();
151+
const buttons = wrapper.findAll("button.btn-outline-primary");
152+
await buttons.at(0).trigger("click");
153+
await flushPromises();
154+
const sel1 = wrapper.vm.sourceContentSelection?.value || wrapper.vm.sourceContentSelection;
155+
expect(sel1["dataset|d1"]).toBe(true);
156+
expect(sel1["dataset|d2"]).toBe(true);
157+
await buttons.at(1).trigger("click");
158+
await flushPromises();
159+
const sel2 = wrapper.vm.sourceContentSelection?.value || wrapper.vm.sourceContentSelection;
160+
expect(sel2["dataset|d1"]).toBe(false);
161+
expect(sel2["dataset|d2"]).toBe(false);
162+
});
163+
164+
it("shows success for single existing target", async () => {
165+
server.use(
166+
http.get("/api/histories", ({ response }) => response(200).json([{ id: "h1", name: "H1" }])),
167+
http.get("/api/histories/{history_id}", ({ params, response }) =>
168+
response(200).json({ id: params.history_id, name: "H1" }),
169+
),
170+
http.get("/api/histories/{history_id}/contents", ({ response }) =>
171+
response(200).json([{ id: "d1", name: "X", hid: 1, history_content_type: "dataset" }]),
172+
),
173+
http.post("/api/histories/{history_id}/copy_contents", ({ response }) =>
174+
response(200).json({ history_ids: ["h1"] }),
175+
),
176+
);
177+
const wrapper = mountComponent();
178+
await flushPromises();
179+
const checkbox = wrapper.find("input[type='checkbox']");
180+
await checkbox.setChecked(true);
181+
await wrapper.find("button.btn-primary").trigger("click");
182+
await flushPromises();
183+
expect(wrapper.text()).toMatch(/1 item[s]? copied to/);
184+
expect(wrapper.text()).toContain("H1");
185+
});
186+
187+
it("shows success for multiple target histories", async () => {
188+
server.use(
189+
http.get("/api/histories", ({ response }) =>
190+
response(200).json([
191+
{ id: "h1", name: "H1" },
192+
{ id: "h2", name: "H2" },
193+
]),
194+
),
195+
http.get("/api/histories/{history_id}", ({ params, response }) =>
196+
response(200).json({ id: params.history_id, name: `History ${params.history_id}` }),
197+
),
198+
http.get("/api/histories/{history_id}/contents", ({ response }) =>
199+
response(200).json([{ id: "d1", name: "X", hid: 1, history_content_type: "dataset" }]),
200+
),
201+
http.post("/api/histories/{history_id}/copy_contents", ({ response }) =>
202+
response(200).json({ history_ids: ["h1", "h2"] }),
203+
),
204+
);
205+
const wrapper = mountComponent();
206+
await flushPromises();
207+
const checkbox = wrapper.find("input[type='checkbox']");
208+
await checkbox.setChecked(true);
209+
wrapper.vm.targetMultiSelections = { h1: true, h2: true };
210+
await wrapper.vm.$nextTick();
211+
await wrapper.find("button.btn-primary").trigger("click");
212+
await flushPromises();
213+
expect(wrapper.text()).toMatch(/1 item[s]? copied to/);
214+
expect(wrapper.text()).toContain("H1");
215+
expect(wrapper.text()).toContain("H2");
216+
});
217+
218+
it("shows success for new history creation", async () => {
219+
server.use(
220+
http.get("/api/histories", ({ response }) => response(200).json([{ id: "h1", name: "H1" }])),
221+
http.get("/api/histories/{history_id}", ({ params, response }) =>
222+
response(200).json({ id: params.history_id, name: "H1" }),
223+
),
224+
http.get("/api/histories/{history_id}/contents", ({ response }) =>
225+
response(200).json([{ id: "d1", name: "X", hid: 1, history_content_type: "dataset" }]),
226+
),
227+
http.post("/api/histories/{history_id}/copy_contents", ({ response }) =>
228+
response(200).json({ history_ids: ["h2"] }),
229+
),
230+
);
231+
const wrapper = await setupBase(
232+
[{ id: "h1", name: "H1" }],
233+
[{ id: "d1", name: "X", hid: 1, history_content_type: "dataset" }],
234+
);
235+
await wrapper.find("input[data-description='copy history name']").setValue("New History");
236+
await wrapper.find("button.btn-primary").trigger("click");
237+
await flushPromises();
238+
expect(wrapper.text()).toContain("1 item copied to");
239+
expect(wrapper.text()).toContain("New History");
240+
});

0 commit comments

Comments
 (0)