Skip to content

Commit b0a0a7f

Browse files
authored
Downloading the instructions (#1160)
## What's Changed? - Includes instructions in the project download if there are any - Adds a list of reserved file names which includes `INSTRUCTIONS.md` to prevent clashes Closes RaspberryPiFoundation/digital-editor-issues#374
1 parent 268c6f6 commit b0a0a7f

File tree

7 files changed

+98
-0
lines changed

7 files changed

+98
-0
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
99
### Added
1010

1111
- Support for the `outputPanels` attribute in the `PyodideRunner` (#1157)
12+
- Downloading project instructions (#1160)
13+
14+
### Changed
15+
16+
- Made `INSTRUCTIONS.md` a reserved file name (#1160)
1217

1318
## [0.28.14] - 2025-01-06
1419

src/components/DownloadButton/DownloadButton.jsx

+4
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ const DownloadButton = (props) => {
3939

4040
const zip = new JSZip();
4141

42+
if (project.instructions) {
43+
zip.file("INSTRUCTIONS.md", project.instructions);
44+
}
45+
4246
project.components.forEach((file) => {
4347
zip.file(`${file.name}.${file.extension}`, file.content);
4448
});

src/components/DownloadButton/DownloadButton.test.js

+58
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ describe("Downloading project with name set", () => {
2525
project: {
2626
name: "My epic project",
2727
identifier: "hello-world-project",
28+
instructions: "print hello world to the console",
2829
components: [
2930
{
3031
name: "main",
@@ -53,6 +54,18 @@ describe("Downloading project with name set", () => {
5354
expect(downloadButton).toBeInTheDocument();
5455
});
5556

57+
test("Clicking download zips instructions", async () => {
58+
fireEvent.click(downloadButton);
59+
const JSZipInstance = JSZip.mock.instances[0];
60+
const mockFile = JSZipInstance.file;
61+
await waitFor(() =>
62+
expect(mockFile).toHaveBeenCalledWith(
63+
"INSTRUCTIONS.md",
64+
"print hello world to the console",
65+
),
66+
);
67+
});
68+
5669
test("Clicking download zips project file content", async () => {
5770
fireEvent.click(downloadButton);
5871
const JSZipInstance = JSZip.mock.instances[0];
@@ -123,3 +136,48 @@ describe("Downloading project with no name set", () => {
123136
);
124137
});
125138
});
139+
140+
describe("Downloading project with no instructions set", () => {
141+
let downloadButton;
142+
143+
beforeEach(() => {
144+
JSZip.mockClear();
145+
const middlewares = [];
146+
const mockStore = configureStore(middlewares);
147+
const initialState = {
148+
editor: {
149+
project: {
150+
name: "My epic project",
151+
identifier: "hello-world-project",
152+
components: [
153+
{
154+
name: "main",
155+
extension: "py",
156+
content: "",
157+
},
158+
],
159+
image_list: [],
160+
},
161+
},
162+
};
163+
const store = mockStore(initialState);
164+
render(
165+
<Provider store={store}>
166+
<DownloadButton buttonText="Download" Icon={() => {}} />
167+
</Provider>,
168+
);
169+
downloadButton = screen.queryByText("Download").parentElement;
170+
});
171+
172+
test("Clicking download button does not zip instructions", async () => {
173+
fireEvent.click(downloadButton);
174+
const JSZipInstance = JSZip.mock.instances[0];
175+
const mockFile = JSZipInstance.file;
176+
await waitFor(() =>
177+
expect(mockFile).not.toHaveBeenCalledWith(
178+
"INSTRUCTIONS.md",
179+
expect.anything(),
180+
),
181+
);
182+
});
183+
});

src/components/Modals/NewFileModal.test.js

+11
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,17 @@ describe("Testing the new file modal", () => {
8181
expect(store.getActions()).toEqual(expectedActions);
8282
});
8383

84+
test("Reserved file name throws error", () => {
85+
fireEvent.change(inputBox, { target: { value: "INSTRUCTIONS.md" } });
86+
fireEvent.click(saveButton);
87+
const expectedActions = [
88+
setNameError("filePanel.errors.reservedFileName", {
89+
fileName: "INSTRUCTIONS.md",
90+
}),
91+
];
92+
expect(store.getActions()).toEqual(expectedActions);
93+
});
94+
8495
test("Unsupported extension throws error", () => {
8596
fireEvent.change(inputBox, { target: { value: "file1.js" } });
8697
fireEvent.click(saveButton);

src/components/Modals/RenameFileModal.test.js

+11
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,17 @@ describe("Testing the rename file modal", () => {
9191
expect(store.getActions()).toEqual(expectedActions);
9292
});
9393

94+
test("Reserved file name throws error", () => {
95+
fireEvent.change(inputBox, { target: { value: "INSTRUCTIONS.md" } });
96+
fireEvent.click(saveButton);
97+
const expectedActions = [
98+
setNameError("filePanel.errors.reservedFileName", {
99+
fileName: "INSTRUCTIONS.md",
100+
}),
101+
];
102+
expect(store.getActions()).toEqual(expectedActions);
103+
});
104+
94105
test("Unchanged file name does not throw error", () => {
95106
fireEvent.click(saveButton);
96107
const expectedActions = [

src/utils/componentNameValidation.js

+7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ const allowedExtensions = {
55
html: ["html", "css", "js"],
66
};
77

8+
const reservedFileNames = ["INSTRUCTIONS.md"];
9+
810
const allowedExtensionsString = (projectType, t) => {
911
const extensionsList = allowedExtensions[projectType];
1012
if (extensionsList.length === 1) {
@@ -19,6 +21,7 @@ const allowedExtensionsString = (projectType, t) => {
1921
const isValidFileName = (fileName, projectType, componentNames) => {
2022
const extension = fileName.split(".").slice(1).join(".");
2123
if (
24+
!reservedFileNames.includes(fileName) &&
2225
allowedExtensions[projectType].includes(extension) &&
2326
!componentNames.includes(fileName) &&
2427
fileName.split(" ").length === 1
@@ -44,6 +47,10 @@ export const validateFileName = (
4447
(currentFileName && fileName === currentFileName)
4548
) {
4649
callback();
50+
} else if (reservedFileNames.includes(fileName)) {
51+
dispatch(
52+
setNameError(t("filePanel.errors.reservedFileName", { fileName })),
53+
);
4754
} else if (componentNames.includes(fileName)) {
4855
dispatch(setNameError(t("filePanel.errors.notUnique")));
4956
} else if (fileName.split(" ").length > 1) {

src/utils/i18n.js

+2
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ i18n
130130
},
131131
filePanel: {
132132
errors: {
133+
reservedFileName:
134+
"{{fileName}} is a reserved file name. Please choose a different name.",
133135
containsSpaces: "File names must not contain spaces.",
134136
generalError: "Error",
135137
notUnique: "File names must be unique.",

0 commit comments

Comments
 (0)