Skip to content

Commit 826c713

Browse files
authored
Make Plot "Export as PNG/JPG" default filenames more clear and make the methods extendable (#8070)
* add filename argument and update default name * add filename argument and update default name * allow the invoke function to accept a filename to pass to the view actions * tests * remove invalid chars from filenames, another test for that * replace periods with underscores as they may be useful in a name
1 parent fa1a45b commit 826c713

File tree

5 files changed

+271
-19
lines changed

5 files changed

+271
-19
lines changed
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
/*****************************************************************************
2+
* Open MCT, Copyright (c) 2014-2024, United States Government
3+
* as represented by the Administrator of the National Aeronautics and Space
4+
* Administration. All rights reserved.
5+
*
6+
* Open MCT is licensed under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
* http://www.apache.org/licenses/LICENSE-2.0.
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
* License for the specific language governing permissions and limitations
15+
* under the License.
16+
*
17+
* Open MCT includes source code licensed under additional open source
18+
* licenses. See the Open Source Licenses file (LICENSES.md) included with
19+
* this source code distribution or the Licensing information page available
20+
* at runtime from the About dialog for additional information.
21+
*****************************************************************************/
22+
23+
/*
24+
Tests to verify log plot functionality when objects are missing
25+
*/
26+
27+
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
28+
import { expect, test } from '../../../../pluginFixtures.js';
29+
30+
const SWG_NAME = 'Sine Wave Generator';
31+
const OVERLAY_PLOT_NAME = 'Overlay Plot';
32+
const STACKED_PLOT_NAME = 'Stacked Plot';
33+
34+
test.describe('For a default Plot View, Plot View Action:', () => {
35+
let download;
36+
37+
test.beforeEach(async ({ page }) => {
38+
await page.goto('./', { waitUntil: 'domcontentloaded' });
39+
40+
const plot = await createDomainObjectWithDefaults(page, {
41+
type: SWG_NAME,
42+
name: SWG_NAME
43+
});
44+
45+
await page.goto(plot.url);
46+
47+
// Set up dialog handler before clicking the export button
48+
await page.getByLabel('More actions').click();
49+
});
50+
51+
test.afterEach(async ({ page }) => {
52+
if (download) {
53+
await download.cancel();
54+
}
55+
});
56+
57+
test('Export as PNG, will suggest the correct default filename', async ({ page }) => {
58+
// Start waiting for download before clicking. Note no await.
59+
const downloadPromise = page.waitForEvent('download');
60+
61+
// trigger the download
62+
await page.getByLabel('Export as PNG').click();
63+
64+
download = await downloadPromise;
65+
66+
// Verify the filename contains the expected pattern
67+
expect(download.suggestedFilename()).toBe(`${SWG_NAME} - plot.png`);
68+
});
69+
70+
test('Export as JPG, will suggest the correct default filename', async ({ page }) => {
71+
// Start waiting for download before clicking. Note no await.
72+
const downloadPromise = page.waitForEvent('download');
73+
74+
// trigger the download
75+
await page.getByLabel('Export as JPG').click();
76+
77+
download = await downloadPromise;
78+
79+
// Verify the filename contains the expected pattern
80+
expect(download.suggestedFilename()).toBe(`${SWG_NAME} - plot.jpeg`);
81+
});
82+
});
83+
84+
test.describe('For an Overlay Plot View, Plot View Action:', () => {
85+
let download;
86+
87+
test.beforeEach(async ({ page }) => {
88+
await page.goto('./', { waitUntil: 'domcontentloaded' });
89+
90+
const overlayPlot = await createDomainObjectWithDefaults(page, {
91+
type: OVERLAY_PLOT_NAME,
92+
name: OVERLAY_PLOT_NAME
93+
});
94+
95+
await createDomainObjectWithDefaults(page, {
96+
type: SWG_NAME,
97+
name: SWG_NAME,
98+
parent: overlayPlot.uuid
99+
});
100+
101+
await page.goto(overlayPlot.url);
102+
103+
// Set up dialog handler before clicking the export button
104+
await page.getByLabel('More actions').click();
105+
});
106+
107+
test.afterEach(async ({ page }) => {
108+
if (download) {
109+
await download.cancel();
110+
}
111+
});
112+
113+
test('Export as PNG, will suggest the correct default filename', async ({ page }) => {
114+
// Start waiting for download before clicking. Note no await.
115+
const downloadPromise = page.waitForEvent('download');
116+
117+
// trigger the download
118+
await page.getByLabel('Export as PNG').click();
119+
120+
download = await downloadPromise;
121+
122+
// Verify the filename contains the expected pattern
123+
expect(download.suggestedFilename()).toBe(`${OVERLAY_PLOT_NAME} - plot.png`);
124+
});
125+
126+
test('Export as JPG, will suggest the correct default filename', async ({ page }) => {
127+
// Start waiting for download before clicking. Note no await.
128+
const downloadPromise = page.waitForEvent('download');
129+
130+
// trigger the download
131+
await page.getByLabel('Export as JPG').click();
132+
133+
download = await downloadPromise;
134+
135+
// Verify the filename contains the expected pattern
136+
expect(download.suggestedFilename()).toBe(`${OVERLAY_PLOT_NAME} - plot.jpeg`);
137+
});
138+
});
139+
140+
test.describe('For a Stacked Plot View, Plot View Action:', () => {
141+
let download;
142+
143+
test.beforeEach(async ({ page }) => {
144+
await page.goto('./', { waitUntil: 'domcontentloaded' });
145+
146+
const stackedPlot = await createDomainObjectWithDefaults(page, {
147+
type: STACKED_PLOT_NAME,
148+
name: STACKED_PLOT_NAME
149+
});
150+
151+
await createDomainObjectWithDefaults(page, {
152+
type: SWG_NAME,
153+
name: SWG_NAME,
154+
parent: stackedPlot.uuid
155+
});
156+
157+
await page.goto(stackedPlot.url);
158+
159+
// Set up dialog handler before clicking the export button
160+
await page.getByLabel('More actions').click();
161+
});
162+
163+
test.afterEach(async ({ page }) => {
164+
if (download) {
165+
await download.cancel();
166+
}
167+
});
168+
169+
test('Export as PNG, will suggest the correct default filename', async ({ page }) => {
170+
// Start waiting for download before clicking. Note no await.
171+
const downloadPromise = page.waitForEvent('download');
172+
173+
// trigger the download
174+
await page.getByLabel('Export as PNG').click();
175+
176+
download = await downloadPromise;
177+
178+
// Verify the filename contains the expected pattern
179+
expect(download.suggestedFilename()).toBe(`${STACKED_PLOT_NAME} - stacked-plot.png`);
180+
});
181+
182+
test('Export as JPG, will suggest the correct default filename', async ({ page }) => {
183+
// Start waiting for download before clicking. Note no await.
184+
const downloadPromise = page.waitForEvent('download');
185+
186+
// trigger the download
187+
await page.getByLabel('Export as JPG').click();
188+
189+
download = await downloadPromise;
190+
191+
// Verify the filename contains the expected pattern
192+
expect(download.suggestedFilename()).toBe(`${STACKED_PLOT_NAME} - stacked-plot.jpeg`);
193+
});
194+
});
195+
196+
test.describe('Plot View Action:', () => {
197+
let download;
198+
199+
test.beforeEach(async ({ page }) => {
200+
await page.goto('./', { waitUntil: 'domcontentloaded' });
201+
202+
const plot = await createDomainObjectWithDefaults(page, {
203+
type: SWG_NAME,
204+
name: `!@#${SWG_NAME}!@#><`
205+
});
206+
207+
await page.goto(plot.url);
208+
209+
// Set up dialog handler before clicking the export button
210+
await page.getByLabel('More actions').click();
211+
});
212+
213+
test.afterEach(async ({ page }) => {
214+
if (download) {
215+
await download.cancel();
216+
}
217+
});
218+
219+
test('Export as PNG saved filenames will not include invalid characters', async ({ page }) => {
220+
// Start waiting for download before clicking. Note no await.
221+
const downloadPromise = page.waitForEvent('download');
222+
223+
// trigger the download
224+
await page.getByLabel('Export as PNG').click();
225+
226+
download = await downloadPromise;
227+
228+
// Verify the filename contains the expected pattern
229+
expect(download.suggestedFilename()).toBe(`${SWG_NAME} - plot.png`);
230+
});
231+
232+
test('Export as JPG saved filenames will not include invalid characters', async ({ page }) => {
233+
// Start waiting for download before clicking. Note no await.
234+
const downloadPromise = page.waitForEvent('download');
235+
236+
// trigger the download
237+
await page.getByLabel('Export as JPG').click();
238+
239+
download = await downloadPromise;
240+
241+
// Verify the filename contains the expected pattern
242+
expect(download.suggestedFilename()).toBe(`${SWG_NAME} - plot.jpeg`);
243+
});
244+
});

src/exporters/ImageExporter.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,14 @@
2525
* Originally created by hudsonfoo on 09/02/16
2626
*/
2727

28-
function replaceDotsWithUnderscores(filename) {
29-
const regex = /\./gi;
28+
function sanitizeFilename(filename) {
29+
const replacedPeriods = filename.replace(/\./g, '_');
30+
const safeFilename = replacedPeriods.replace(/[^a-zA-Z0-9_\-.\s]/g, '');
3031

31-
return filename.replace(regex, '_');
32+
// Handle leading/trailing spaces and periods
33+
const trimmedFilename = safeFilename.trim().replace(/^\.+|\.+$/g, '');
34+
35+
return trimmedFilename;
3236
}
3337

3438
import { saveAs } from 'file-saver';
@@ -150,7 +154,7 @@ class ImageExporter {
150154
* @returns {promise}
151155
*/
152156
async exportJPG(element, filename, className) {
153-
const processedFilename = replaceDotsWithUnderscores(filename);
157+
const processedFilename = sanitizeFilename(filename);
154158

155159
const img = await this.renderElement(element, {
156160
imageType: 'jpg',
@@ -167,7 +171,7 @@ class ImageExporter {
167171
* @returns {promise}
168172
*/
169173
async exportPNG(element, filename, className) {
170-
const processedFilename = replaceDotsWithUnderscores(filename);
174+
const processedFilename = sanitizeFilename(filename);
171175

172176
const img = await this.renderElement(element, {
173177
imageType: 'png',

src/plugins/plot/PlotView.vue

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -202,13 +202,17 @@ export default {
202202
this.imageExporter = null;
203203
this.stopListening();
204204
},
205-
exportJPG() {
205+
exportJPG(filename) {
206206
const plotElement = this.$refs.plotContainer;
207-
this.imageExporter.exportJPG(plotElement, 'plot.jpg', 'export-plot');
207+
filename = filename ?? `${this.domainObject.name} - plot`;
208+
209+
this.imageExporter.exportJPG(plotElement, filename, 'export-plot');
208210
},
209-
exportPNG() {
211+
exportPNG(filename) {
210212
const plotElement = this.$refs.plotContainer;
211-
this.imageExporter.exportPNG(plotElement, 'plot.png', 'export-plot');
213+
filename = filename ?? `${this.domainObject.name} - plot`;
214+
215+
this.imageExporter.exportPNG(plotElement, filename, 'export-plot');
212216
},
213217
setStatus(status) {
214218
this.status = status;

src/plugins/plot/actions/ViewActions.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ const exportPNG = {
2727
description: "Export This View's Data as PNG",
2828
cssClass: 'icon-download',
2929
group: 'view',
30-
invoke(objectPath, view) {
31-
view.getViewContext().exportPNG();
30+
invoke(objectPath, view, filename) {
31+
view.getViewContext().exportPNG(filename);
3232
}
3333
};
3434

@@ -38,8 +38,8 @@ const exportJPG = {
3838
description: "Export This View's Data as JPG",
3939
cssClass: 'icon-download',
4040
group: 'view',
41-
invoke(objectPath, view) {
42-
view.getViewContext().exportJPG();
41+
invoke(objectPath, view, filename) {
42+
view.getViewContext().exportJPG(filename);
4343
}
4444
};
4545

src/plugins/plot/stackedPlot/StackedPlot.vue

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -271,23 +271,23 @@ export default {
271271
this.compositionObjects = [];
272272
},
273273
274-
exportJPG() {
274+
exportJPG(filename) {
275275
this.hideExportButtons = true;
276276
const plotElement = this.$el;
277+
filename = filename ?? `${this.domainObject.name} - stacked-plot`;
277278
278-
this.imageExporter.exportJPG(plotElement, 'stacked-plot.jpg', 'export-plot').finally(
279+
this.imageExporter.exportJPG(plotElement, filename, 'export-plot').finally(
279280
function () {
280281
this.hideExportButtons = false;
281282
}.bind(this)
282283
);
283284
},
284285
285-
exportPNG() {
286+
exportPNG(filename) {
286287
this.hideExportButtons = true;
287-
288288
const plotElement = this.$el;
289-
290-
this.imageExporter.exportPNG(plotElement, 'stacked-plot.png', 'export-plot').finally(
289+
filename = filename ?? `${this.domainObject.name} - stacked-plot`;
290+
this.imageExporter.exportPNG(plotElement, filename, 'export-plot').finally(
291291
function () {
292292
this.hideExportButtons = false;
293293
}.bind(this)

0 commit comments

Comments
 (0)