Skip to content

Commit 71c1768

Browse files
authored
Merge branch 'main' into 419/sidebar
2 parents dddac79 + 0b93279 commit 71c1768

17 files changed

+531
-74
lines changed

CHANGELOG.md

+25-1
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,31 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66

77
## Unreleased
88

9+
### Added
10+
11+
- Fake translation for stress testing (#1206)
12+
913
### Fixed
1014
- Styling issue on sidebar on mobile (#1194)
1115

16+
## [0.30.0] - 2025-04-15
17+
18+
### Added
19+
20+
- `editor-projectLoadFailed` custom event that fires when a project completely fails to load (#1201)
21+
- Added runnerBeingLoaded state to prevent race condition overwrites (#1205)
22+
23+
### Fixed
24+
25+
- Bugs in append mode for writing to files in python (#1200)
26+
- `turtle` bug that did not display output on first code run (#1203)
27+
28+
## [0.29.1] - 2025-02-21
29+
30+
### Fixed
31+
32+
- Fixed sidebar not correctly reopening (#1196)
33+
1234
## [0.29.0] - 2025-02-06
1335

1436
### Added
@@ -1065,7 +1087,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
10651087

10661088
- Events in Web Component indicating whether Mission Zero criteria have been met (#113)
10671089

1068-
[unreleased]: https://github.com/RaspberryPiFoundation/editor-ui/compare/v0.29.0...HEAD
1090+
[unreleased]: https://github.com/RaspberryPiFoundation/editor-ui/compare/v0.29.1...HEAD
1091+
[0.30.0]: https://github.com/RaspberryPiFoundation/editor-ui/releases/tag/v0.30.0
1092+
[0.29.1]: https://github.com/RaspberryPiFoundation/editor-ui/releases/tag/v0.29.1
10691093
[0.29.0]: https://github.com/RaspberryPiFoundation/editor-ui/releases/tag/v0.29.0
10701094
[0.28.14]: https://github.com/RaspberryPiFoundation/editor-ui/releases/tag/v0.28.14
10711095
[0.28.13]: https://github.com/RaspberryPiFoundation/editor-ui/releases/tag/v0.28.13

cypress/e2e/spec-wc-skulpt.cy.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ describe("Running the code with skulpt", () => {
3838
.shadow()
3939
.find(".skulptrunner")
4040
.contains(".react-tabs__tab", "Visual output")
41-
.should("not.exist");
41+
.should("not.be.visible");
4242
cy.get("editor-wc")
4343
.shadow()
4444
.find(".skulptrunner")

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@raspberrypifoundation/editor-ui",
3-
"version": "0.29.0",
3+
"version": "0.30.0",
44
"private": true,
55
"dependencies": {
66
"@apollo/client": "^3.7.8",

src/PyodideWorker.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ const PyodideWorker = () => {
116116
self.content += content
117117
if len(self.content) > MAX_FILE_SIZE:
118118
raise OSError(f"File '{self.filename}' exceeds maximum file size of {MAX_FILE_SIZE} bytes")
119-
with _original_open(self.filename, "w") as f:
119+
with _original_open(self.filename, mode) as f:
120120
f.write(self.content)
121121
basthon.kernel.write_file({ "filename": self.filename, "content": self.content, "mode": mode })
122122

src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.jsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ const PyodideRunner = ({ active, outputPanels = ["text", "visual"] }) => {
223223
updatedContent = content;
224224
} else if (mode === "a") {
225225
updatedContent =
226-
(componentToUpdate ? componentToUpdate.content + "\n" : "") + content;
226+
(componentToUpdate ? componentToUpdate.content : "") + content;
227227
}
228228

229229
if (componentToUpdate) {

src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ describe("When file write event is received", () => {
307307
worker.postMessageFromWorker({
308308
method: "handleFileWrite",
309309
filename: "existing_file.txt",
310-
content: "new content",
310+
content: "\nnew content",
311311
mode: "a",
312312
});
313313
expect(dispatchSpy).toHaveBeenCalledWith({
@@ -364,7 +364,7 @@ describe("When file write event is received", () => {
364364
worker.postMessageFromWorker({
365365
method: "handleFileWrite",
366366
filename: "existing_file.txt",
367-
content: "new content",
367+
content: "\nnew content",
368368
mode: "a",
369369
});
370370
expect(dispatchSpy).toHaveBeenCalledWith({

src/components/Editor/Runners/PythonRunner/PythonRunner.jsx

+2-23
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useDispatch, useSelector } from "react-redux";
66
import { useTranslation } from "react-i18next";
77

88
import { loadingRunner } from "../../../../redux/EditorSlice";
9+
import { getPythonImports } from "../../../../utils/getPythonImports";
910

1011
const SKULPT_ONLY_MODULES = [
1112
"p5",
@@ -45,33 +46,11 @@ const PythonRunner = ({ outputPanels = ["text", "visual"] }) => {
4546
setSkulptFallback(true);
4647
return;
4748
}
48-
const getImports = (code) => {
49-
const codeWithoutMultilineStrings = code.replace(
50-
/'''[\s\S]*?'''|"""[\s\S]*?"""/gm,
51-
"",
52-
);
53-
const importRegex =
54-
/(?<=^\s*)(from\s+([a-zA-Z0-9_.]+)(\s+import\s+([a-zA-Z0-9_.]+))?)|(?<=^\s*)(import\s+([a-zA-Z0-9_.]+))/gm;
55-
const matches = codeWithoutMultilineStrings.match(importRegex);
56-
const imports = matches
57-
? matches.map(
58-
(match) =>
59-
match
60-
.split(/from|import/)
61-
.filter(Boolean)
62-
.map((s) => s.trim())[0],
63-
)
64-
: [];
65-
if (code.includes(`# ${t("input.comment.py5")}`)) {
66-
imports.push("py5_imported");
67-
}
68-
return imports;
69-
};
7049

7150
for (const component of project.components || []) {
7251
if (component.extension === "py" && !codeRunTriggered) {
7352
try {
74-
const imports = getImports(component.content);
53+
const imports = getPythonImports(component.content, t);
7554
const hasSkulptOnlyModules = imports.some((name) =>
7655
SKULPT_ONLY_MODULES.includes(name),
7756
);

src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.jsx

+68-29
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import OutputViewToggle from "../OutputViewToggle";
2424
import { SettingsContext } from "../../../../../utils/settings";
2525
import RunnerControls from "../../../../RunButton/RunnerControls";
2626
import { MOBILE_MEDIA_QUERY } from "../../../../../utils/mediaQueryBreakpoints";
27+
import { getPythonImports } from "../../../../../utils/getPythonImports";
2728

2829
const externalLibraries = {
2930
"./pygal/__init__.js": {
@@ -55,6 +56,15 @@ const externalLibraries = {
5556
},
5657
};
5758

59+
const VISUAL_LIBRARIES = [
60+
"pygal",
61+
"py5",
62+
"py5_imported",
63+
"p5",
64+
"sense_hat",
65+
"turtle",
66+
];
67+
5868
const SkulptRunner = ({ active, outputPanels = ["text", "visual"] }) => {
5969
const loadedRunner = useSelector((state) => state.editor.loadedRunner);
6070
const projectCode = useSelector((state) => state.editor.project.components);
@@ -83,10 +93,31 @@ const SkulptRunner = ({ active, outputPanels = ["text", "visual"] }) => {
8393
const settings = useContext(SettingsContext);
8494
const isMobile = useMediaQuery({ query: MOBILE_MEDIA_QUERY });
8595

96+
const project = useSelector((state) => state.editor.project);
97+
98+
const testForVisualImports = (project) => {
99+
for (const component of project.components || []) {
100+
if (component.extension === "py") {
101+
try {
102+
const imports = getPythonImports(component.content, t);
103+
const hasVisualImports = imports.some((name) =>
104+
VISUAL_LIBRARIES.includes(name),
105+
);
106+
if (hasVisualImports) {
107+
return true;
108+
}
109+
} catch (error) {
110+
console.error("Error occurred while getting imports:", error);
111+
}
112+
}
113+
}
114+
return false;
115+
};
116+
86117
const [codeHasVisualOutput, setCodeHasVisualOutput] = useState(
87-
senseHatAlwaysEnabled,
118+
!!senseHatAlwaysEnabled || testForVisualImports(project),
88119
);
89-
const [showVisualOutput, setShowVisualOutput] = useState(true);
120+
const [showVisualOutput, setShowVisualOutput] = useState(codeHasVisualOutput);
90121

91122
const getInput = () => {
92123
const pageInput = document.getElementById("input");
@@ -96,6 +127,14 @@ const SkulptRunner = ({ active, outputPanels = ["text", "visual"] }) => {
96127
return pageInput || webComponentInput;
97128
};
98129

130+
useEffect(() => {
131+
if (!codeRunTriggered) {
132+
setCodeHasVisualOutput(
133+
!!senseHatAlwaysEnabled || testForVisualImports(project),
134+
);
135+
}
136+
}, [project, codeRunTriggered, senseHatAlwaysEnabled, t]);
137+
99138
useEffect(() => {
100139
if (active && loadedRunner !== "skulpt") {
101140
dispatch(setLoadedRunner("skulpt"));
@@ -136,15 +175,6 @@ const SkulptRunner = ({ active, outputPanels = ["text", "visual"] }) => {
136175
}
137176
}, [drawTriggered, codeRunTriggered]);
138177

139-
const visualLibraries = [
140-
"./pygal/__init__.js",
141-
"./py5/__init__.js",
142-
"./py5_imported/__init__.js",
143-
"./p5/__init__.js",
144-
"./_internal_sense_hat/__init__.js",
145-
"src/builtin/turtle/__init__.js",
146-
];
147-
148178
const outf = (text) => {
149179
if (text !== "") {
150180
const node = output.current;
@@ -169,10 +199,6 @@ const SkulptRunner = ({ active, outputPanels = ["text", "visual"] }) => {
169199

170200
// TODO: Handle pre-importing py5_imported when refactored py5 shim imported
171201

172-
if (visualLibraries.includes(library)) {
173-
setCodeHasVisualOutput(true);
174-
}
175-
176202
let localProjectFiles = projectCode
177203
.filter((component) => component.name !== "main")
178204
.map((component) => `./${component.name}.py`);
@@ -436,6 +462,18 @@ const SkulptRunner = ({ active, outputPanels = ["text", "visual"] }) => {
436462
});
437463
};
438464

465+
const [tabbedViewSelectedIndex, setTabbedViewSelectedIndex] = useState(
466+
showVisualOutput ? 0 : 1,
467+
);
468+
469+
useEffect(() => {
470+
if (showVisualOutput) {
471+
setTabbedViewSelectedIndex(0);
472+
} else {
473+
setTabbedViewSelectedIndex(1);
474+
}
475+
}, [showVisualOutput]);
476+
439477
return (
440478
<div
441479
className={classNames("pythonrunner-container", "skulptrunner", {
@@ -444,8 +482,11 @@ const SkulptRunner = ({ active, outputPanels = ["text", "visual"] }) => {
444482
>
445483
{isSplitView || singleOutputPanel ? (
446484
<>
447-
{showVisualOutput && showVisualOutputPanel && (
448-
<div className={outputPanelClasses("visual")}>
485+
{showVisualOutputPanel && (
486+
<div
487+
className={outputPanelClasses("visual")}
488+
style={{ blockSize: showVisualOutput ? "auto" : 0 }}
489+
>
449490
<Tabs forceRenderTabPanel={true}>
450491
<div
451492
className={classNames("react-tabs__tab-container", {
@@ -503,16 +544,16 @@ const SkulptRunner = ({ active, outputPanels = ["text", "visual"] }) => {
503544
<Tabs
504545
forceRenderTabPanel={true}
505546
defaultIndex={showVisualOutput ? 0 : 1}
547+
selectedIndex={tabbedViewSelectedIndex}
548+
onSelect={setTabbedViewSelectedIndex}
506549
>
507550
<div className="react-tabs__tab-container">
508551
<TabList>
509-
{showVisualOutput ? (
510-
<Tab key={0}>
511-
<span className="react-tabs__tab-text">
512-
{t("output.visualOutput")}
513-
</span>
514-
</Tab>
515-
) : null}
552+
<Tab key={0} style={{ blockSize: showVisualOutput ? "auto" : 0 }}>
553+
<span className="react-tabs__tab-text">
554+
{t("output.visualOutput")}
555+
</span>
556+
</Tab>
516557
<Tab key={1}>
517558
<span className="react-tabs__tab-text">
518559
{t("output.textOutput")}
@@ -523,11 +564,9 @@ const SkulptRunner = ({ active, outputPanels = ["text", "visual"] }) => {
523564
{!isEmbedded && isMobile && <RunnerControls skinny />}
524565
</div>
525566
{!isOutputOnly && <ErrorMessage />}
526-
{showVisualOutput ? (
527-
<TabPanel key={0}>
528-
<VisualOutputPane />
529-
</TabPanel>
530-
) : null}
567+
<TabPanel key={0}>
568+
<VisualOutputPane />
569+
</TabPanel>
531570
<TabPanel key={1}>
532571
<pre
533572
className={`pythonrunner-console pythonrunner-console--${settings.fontSize}`}

src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.test.js

+10-9
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,7 @@ describe("When in split view, py5_imported imported and code run", () => {
428428
{
429429
name: "main",
430430
extension: "py",
431-
content: "import py5_imported",
431+
content: "# input.comment.py5",
432432
},
433433
],
434434
image_list: [],
@@ -563,7 +563,7 @@ describe("When in split view, sense_hat imported and code run", () => {
563563
{
564564
name: "main",
565565
extension: "py",
566-
content: "import _internal_sense_hat",
566+
content: "import sense_hat",
567567
},
568568
],
569569
image_list: [],
@@ -675,7 +675,7 @@ describe("When in tabbed view, py5 imported and code run", () => {
675675
));
676676
});
677677

678-
test("Output view toggle not shown", () => {
678+
test("Output view toggle is shown", () => {
679679
expect(
680680
screen.queryByText("outputViewToggle.buttonSplitLabel"),
681681
).toBeInTheDocument();
@@ -704,7 +704,7 @@ describe("When in tabbed view, py5_imported imported and code run", () => {
704704
{
705705
name: "main",
706706
extension: "py",
707-
content: "import py5_imported",
707+
content: "# input.comment.py5",
708708
},
709709
],
710710
image_list: [],
@@ -724,7 +724,7 @@ describe("When in tabbed view, py5_imported imported and code run", () => {
724724
));
725725
});
726726

727-
test("Output view toggle not shown", () => {
727+
test("Output view toggle is shown", () => {
728728
expect(
729729
screen.queryByText("outputViewToggle.buttonSplitLabel"),
730730
).toBeInTheDocument();
@@ -769,7 +769,7 @@ describe("When in tabbed view, pygal imported and code run", () => {
769769
));
770770
});
771771

772-
test("Output view toggle not shown", () => {
772+
test("Output view toggle is shown", () => {
773773
expect(
774774
screen.queryByText("outputViewToggle.buttonSplitLabel"),
775775
).toBeInTheDocument();
@@ -814,7 +814,8 @@ describe("When in tabbed view, turtle imported and code run", () => {
814814
));
815815
});
816816

817-
test("Output view toggle not shown", () => {
817+
test("Output view toggle is shown", () => {
818+
screen.debug();
818819
expect(
819820
screen.queryByText("outputViewToggle.buttonSplitLabel"),
820821
).toBeInTheDocument();
@@ -839,7 +840,7 @@ describe("When in tabbed view, sense_hat imported and code run", () => {
839840
{
840841
name: "main",
841842
extension: "py",
842-
content: "import _internal_sense_hat",
843+
content: "import sense_hat",
843844
},
844845
],
845846
image_list: [],
@@ -859,7 +860,7 @@ describe("When in tabbed view, sense_hat imported and code run", () => {
859860
));
860861
});
861862

862-
test("Output view toggle not shown", () => {
863+
test("Output view toggle is shown", () => {
863864
expect(
864865
screen.queryByText("outputViewToggle.buttonSplitLabel"),
865866
).toBeInTheDocument();

0 commit comments

Comments
 (0)