Skip to content

Commit f2b86b4

Browse files
committed
fix: add block loading and saving
1 parent f2f4809 commit f2b86b4

8 files changed

Lines changed: 321 additions & 8 deletions

File tree

src/App.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,39 @@ import BlocklyEditorFixer from "./components/topLevel/BlockyEditorFixer";
88
import Monitor from "./components/topLevel/Monitor";
99
import "./customBlocks/customBlocks";
1010
import CodeResult from "./components/topLevel/CodeResult";
11+
import { useState } from "react";
1112

1213

1314
function App() {
15+
const [currentWorkspaceJson, setCurrentWorkspaceJson] = useState<object>({});
16+
const [externalJson, setExternalJson] = useState<object | undefined>(undefined);
17+
18+
const handleWorkspaceChange = (workspaceJson: object) => {
19+
setCurrentWorkspaceJson(workspaceJson);
20+
};
21+
22+
const handleLoadWorkspace = (workspaceJson: object) => {
23+
setExternalJson(workspaceJson);
24+
// Reset external JSON after it's been applied
25+
setTimeout(() => setExternalJson(undefined), 200);
26+
};
27+
1428
return (
1529
<DeviceProvider >
1630
<GenerateCodeProvider>
1731
<div className="flex flex-col h-full w-full">
1832
<Header />
19-
<TopBar />
33+
<TopBar
34+
currentWorkspaceJson={currentWorkspaceJson}
35+
onLoadWorkspace={handleLoadWorkspace}
36+
/>
2037
<div className="grid grid-cols-3 gap-2 h-full">
2138
<div className="col-span-2 min-h-full">
2239
<BlocklyEditorFixer />
23-
<BlocklyEditor />
40+
<BlocklyEditor
41+
onWorkspaceChange={handleWorkspaceChange}
42+
externalJson={externalJson}
43+
/>
2444
</div>
2545
<div className="col-span-1 flex flex-col">
2646
<div className="w-full">

src/components/buttons/LoadBtn.tsx

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { FC, InputHTMLAttributes, useState } from "react";
2+
import Button, { classNamesOverride } from "./Button";
3+
import { loadWorkspaceFromFile, isValidWorkspaceJson } from "../../utils/blocklyStorage";
4+
5+
export interface LoadBtnProps extends InputHTMLAttributes<HTMLInputElement> {
6+
onLoadWorkspace: (workspaceJson: object) => void;
7+
}
8+
9+
const LoadBtn: FC<LoadBtnProps> = ({ onLoadWorkspace }) => {
10+
const [loading, setLoading] = useState(false);
11+
const [loaded, setLoaded] = useState(false);
12+
13+
const handleLoad = async () => {
14+
try {
15+
setLoading(true);
16+
17+
const workspaceJson = await loadWorkspaceFromFile();
18+
19+
// Validate the loaded JSON
20+
if (!isValidWorkspaceJson(workspaceJson)) {
21+
throw new Error('Invalid workspace format');
22+
}
23+
24+
// Call the callback to update the workspace
25+
onLoadWorkspace(workspaceJson);
26+
27+
setLoaded(true);
28+
setTimeout(() => setLoaded(false), 2000); // Reset after 2 seconds
29+
} catch (error) {
30+
console.error('Failed to load blocks:', error);
31+
if (error instanceof Error) {
32+
if (error.message === 'File selection cancelled') {
33+
// User cancelled file selection, don't show error
34+
return;
35+
}
36+
alert(`Failed to load blocks: ${error.message}`);
37+
} else {
38+
alert('Failed to load blocks. Please ensure you selected a valid JSON file.');
39+
}
40+
} finally {
41+
setLoading(false);
42+
}
43+
};
44+
45+
const getButtonText = () => {
46+
if (loading) return "Loading...";
47+
if (loaded) return "Loaded!";
48+
return "Load Blocks";
49+
};
50+
51+
const getButtonClass = () => {
52+
if (loaded) return "bg-green-500 hover:bg-green-600";
53+
if (loading) return "bg-yellow-500 hover:bg-yellow-600";
54+
return "";
55+
};
56+
57+
return (
58+
<Button
59+
classNames={classNamesOverride(getButtonClass())}
60+
text={getButtonText()}
61+
active={!loading}
62+
onClick={handleLoad}
63+
disabled={loading}
64+
/>
65+
);
66+
};
67+
68+
export default LoadBtn;

src/components/buttons/SaveBtn.tsx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { FC, InputHTMLAttributes, useState } from "react";
2+
import Button, { classNamesOverride } from "./Button";
3+
import { saveWorkspaceToFile } from "../../utils/blocklyStorage";
4+
5+
export interface SaveBtnProps extends InputHTMLAttributes<HTMLInputElement> {
6+
workspaceJson: object;
7+
}
8+
9+
const SaveBtn: FC<SaveBtnProps> = ({ workspaceJson }) => {
10+
const [saving, setSaving] = useState(false);
11+
const [saved, setSaved] = useState(false);
12+
13+
const handleSave = async () => {
14+
try {
15+
setSaving(true);
16+
17+
// Generate filename with timestamp
18+
const now = new Date();
19+
const timestamp = now.toISOString().slice(0, 19).replace(/[:.]/g, '-');
20+
const filename = `blocks-${timestamp}.json`;
21+
22+
await saveWorkspaceToFile(workspaceJson, filename);
23+
24+
setSaved(true);
25+
setTimeout(() => setSaved(false), 2000); // Reset after 2 seconds
26+
} catch (error) {
27+
console.error('Failed to save blocks:', error);
28+
alert('Failed to save blocks. Please try again.');
29+
} finally {
30+
setSaving(false);
31+
}
32+
};
33+
34+
const getButtonText = () => {
35+
if (saving) return "Saving...";
36+
if (saved) return "Saved!";
37+
return "Save Blocks";
38+
};
39+
40+
const getButtonClass = () => {
41+
if (saved) return "bg-green-500 hover:bg-green-600";
42+
if (saving) return "bg-yellow-500 hover:bg-yellow-600";
43+
return "";
44+
};
45+
46+
return (
47+
<Button
48+
classNames={classNamesOverride(getButtonClass())}
49+
text={getButtonText()}
50+
active={!saving}
51+
onClick={handleSave}
52+
disabled={saving}
53+
/>
54+
);
55+
};
56+
57+
export default SaveBtn;

src/components/topLevel/BlocklyEditor.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {FC, InputHTMLAttributes, useRef, useState} from "react";
1+
import {FC, InputHTMLAttributes, useRef, useState, useEffect} from "react";
22
import {useGenerateCode} from "../../context/GenerateCodeContext";
33
import Blockly from "blockly";
44
import {javascriptGenerator} from "blockly/javascript";
@@ -7,14 +7,28 @@ import "./../../customBlocks/customBlocks";
77

88
import {toolbox} from "../../customBlocks/toolbox";
99
export interface HeaderProps extends InputHTMLAttributes<HTMLInputElement> {
10+
onWorkspaceChange?: (workspaceJson: object) => void;
11+
externalJson?: object;
1012
}
1113

1214
const defaultJson = '{"blocks":{"languageVersion":0,"blocks":[{"type":"variables_set","id":"Y?k]s#{Xr~,YqBs2fh]t","x":130,"y":170,"fields":{"VAR":{"id":"C(8;cYCF}~vSgkxzJ+{O"}},"inputs":{"VALUE":{"block":{"type":"math_number","id":"C:U)n7kMTy+b,XG{uxL^","fields":{"NUM":0}}}},"next":{"block":{"type":"set_interval","id":"F%q?js8I#%9/ki7ne!|;","inputs":{"NAME":{"shadow":{"type":"text","id":"=_68LlU9{7okadzqzuEh","fields":{"TEXT":""}}},"CODE":{"block":{"type":"console","id":"XO60!gMqEwBhnZ.p)IHd","fields":{"TYPE":"log"},"inputs":{"TEXT":{"shadow":{"type":"text","id":"fNM*#(#5*~Q#vGQ!QZ2!","fields":{"TEXT":""}},"block":{"type":"text_join","id":":t|fc3dH_+UPH|4V]VyS","extraState":{"itemCount":2},"inputs":{"ADD0":{"block":{"type":"text","id":"mNfX8`}O`4Q]H.!G_]~8","fields":{"TEXT":"JacLy - index: "}}},"ADD1":{"block":{"type":"variables_get","id":"E[o(]lO4#gYq5t-aCp.{","fields":{"VAR":{"id":"C(8;cYCF}~vSgkxzJ+{O"}}}}}}}},"next":{"block":{"type":"math_change","id":"_t{5oErJ:8+j.S3~?G;{","fields":{"VAR":{"id":"C(8;cYCF}~vSgkxzJ+{O"}},"inputs":{"DELTA":{"shadow":{"type":"math_number","id":"t]D:_fB#0@yvgTT=3Zy4","fields":{"NUM":1}}}}}}}},"INTERVAL":{"shadow":{"type":"math_number","id":"]~,3m4c`7d-s3PqgU?*~","fields":{"NUM":1000}}}}}}}]},"variables":[{"name":"i","id":"C(8;cYCF}~vSgkxzJ+{O"}]}'
1315

14-
const BlocklyEditor: FC<HeaderProps> = ({}) => {
16+
const BlocklyEditor: FC<HeaderProps> = ({ onWorkspaceChange, externalJson }) => {
1517

1618
const {setCode} = useGenerateCode();
1719
const [json, setJson] = useState<object>(JSON.parse(localStorage.getItem("blockly") || defaultJson));
20+
const [workspaceKey, setWorkspaceKey] = useState<number>(0);
21+
22+
// Effect to handle external JSON loading
23+
useEffect(() => {
24+
if (externalJson) {
25+
setJson(externalJson);
26+
localStorage.setItem("blockly", JSON.stringify(externalJson));
27+
// Force re-mount of BlocklyWorkspace component by changing the key
28+
setWorkspaceKey(prev => prev + 1);
29+
}
30+
}, [externalJson]);
31+
1832
const handleWorkspaceChange = (newWorkspace: Blockly.WorkspaceSvg) => {
1933
console.log("Workspace changed")
2034
try {
@@ -29,10 +43,15 @@ const BlocklyEditor: FC<HeaderProps> = ({}) => {
2943
setJson(newJson);
3044
// save to local storage
3145
localStorage.setItem("blockly", JSON.stringify(newJson));
46+
// Notify parent component of workspace changes
47+
if (onWorkspaceChange) {
48+
onWorkspaceChange(newJson);
49+
}
3250
}
3351

3452
return (
3553
<BlocklyWorkspace
54+
key={workspaceKey} // Force re-mount when key changes
3655
className="w-full h-full"
3756
toolboxConfiguration={toolbox}
3857
onWorkspaceChange={handleWorkspaceChange}

src/components/topLevel/TopBar.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,16 @@ import StopBtn from "../buttons/StopBtn";
66
import RestartBtn from "../buttons/RestartBtn";
77
import CopyBtn from "../buttons/CopyBtn";
88
import UploadLibsBtn from "../buttons/UploadLibsBtn";
9+
import SaveBtn from "../buttons/SaveBtn";
10+
import LoadBtn from "../buttons/LoadBtn";
11+
912
export interface buttonProps extends InputHTMLAttributes<HTMLInputElement> {
13+
currentWorkspaceJson?: object;
14+
onLoadWorkspace?: (workspaceJson: object) => void;
1015
}
1116

1217

13-
const ConnectionBar: FC<buttonProps> = ({}) => {
18+
const ConnectionBar: FC<buttonProps> = ({ currentWorkspaceJson = {}, onLoadWorkspace }) => {
1419

1520
return (
1621
<div className="white_text w-full bg-gray-200 rounded p-2 mb-2 flex flex-row gap-2">
@@ -22,6 +27,9 @@ const ConnectionBar: FC<buttonProps> = ({}) => {
2227
<StopBtn/>
2328
<RestartBtn/>
2429
<div className="w-2"></div>
30+
<SaveBtn workspaceJson={currentWorkspaceJson} />
31+
{onLoadWorkspace && <LoadBtn onLoadWorkspace={onLoadWorkspace} />}
32+
<div className="w-2"></div>
2533
<div className="flex flex-grow"></div>
2634
<CopyBtn/>
2735
</div>

src/customBlocks/categories/Robutek.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ Blockly.Blocks['robutek_setSpeed'] = {
128128
}
129129

130130
javascriptGenerator.forBlock['robutek_setSpeed'] = function (b: BlockSvg, g: CodeGenerator) {
131-
var speed = getField(b, 'SPEED') || '0';
131+
var speed = getVal(g, b, 'SPEED') || '0';
132132
return "robutek.setSpeed(" + speed + ");\n";
133133
}
134134

@@ -159,7 +159,7 @@ Blockly.Blocks['robutek_setRamp'] = {
159159
}
160160

161161
javascriptGenerator.forBlock['robutek_setRamp'] = function (b: BlockSvg, g: CodeGenerator) {
162-
var ramp = getField(b, 'RAMP') || '0';
162+
var ramp = getVal(g, b, 'RAMP') || '0';
163163
return "robutek.setRamp(" + ramp + ");\n";
164164
}
165165

@@ -293,4 +293,34 @@ Blockly.Blocks['robutek_rotate'] = {
293293

294294
javascriptGenerator.forBlock['robutek_rotate'] = function (b: BlockSvg, g: CodeGenerator) {
295295
return 'robutek.rotate(' + getVal(g, b, 'ANGLE') + ');\n';
296+
}
297+
298+
// ---- //
299+
300+
// Robutek stop(break:boolean)
301+
addItemToToolbox(toolbox, "Robutek",
302+
{
303+
kind: "block",
304+
blockxml:
305+
' <block type="robutek_stop">\n' +
306+
' <value name="BREAK">\n' +
307+
' <shadow type="logic_boolean">\n' +
308+
' <field name="BOOL">FALSE</field>\n' +
309+
" </shadow>\n" +
310+
" </value>\n" +
311+
" </block>\n",
312+
},
313+
);
314+
315+
Blockly.Blocks['robutek_stop'] = {
316+
init: function () {
317+
dummy(this, 'robutek.stop');
318+
value(this, "BREAK", "");
319+
inline(this);
320+
color(this, "Robutek");
321+
}
322+
}
323+
324+
javascriptGenerator.forBlock['robutek_stop'] = function (b: BlockSvg, g: CodeGenerator) {
325+
return 'robutek.stop(' + getVal(g, b, 'BREAK') + ');\n';
296326
}

src/customBlocks/categories/Servo.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Blockly.Blocks['servo_import'] = {
2222
}
2323

2424
javascriptGenerator.forBlock['servo_import'] = function (b: BlockSvg, g: CodeGenerator) {
25-
return "import * as servo from './libs/servo.js';\n"
25+
return "import { Servo } from './libs/servo.js';\n"
2626
}
2727

2828
// ---- //

0 commit comments

Comments
 (0)