Skip to content

Commit a3d8680

Browse files
authored
Merge branch 'main' into pr_backend_serve
2 parents d8cfd5f + c297f46 commit a3d8680

25 files changed

+493
-272
lines changed

src/App.tsx

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import ToolboxSettingsModal from './reactComponents/ToolboxSettings';
3131
import * as Tabs from './reactComponents/Tabs';
3232
import { TabType } from './types/TabType';
3333

34-
import * as editor from './editor/editor';
3534
import { extendedPythonGenerator } from './editor/extended_python_generator';
3635

3736
import * as commonStorage from './storage/common_storage';
@@ -42,7 +41,6 @@ import * as clientSideStorage from './storage/client_side_storage';
4241
import * as CustomBlocks from './blocks/setup_custom_blocks';
4342

4443
import { initialize as initializePythonBlocks } from './blocks/utils/python';
45-
import { TOOLBOX_UPDATE_EVENT } from './blocks/mrc_mechanism_component_holder';
4644
import { antdThemeFromString } from './reactComponents/ThemeModal';
4745
import { useTranslation } from 'react-i18next';
4846
import { UserSettingsProvider } from './reactComponents/UserSettingsProvider';
@@ -261,24 +259,6 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
261259
handleToolboxSettingsOk(updatedShownCategories);
262260
};
263261

264-
/** Handles toolbox update requests from blocks */
265-
const handleToolboxUpdateRequest = React.useCallback((e: Event) => {
266-
const workspaceId = (e as CustomEvent).detail.workspaceId;
267-
const correspondingEditor = editor.Editor.getEditorForBlocklyWorkspaceId(workspaceId);
268-
if (correspondingEditor) {
269-
correspondingEditor.updateToolbox(shownPythonToolboxCategories);
270-
}
271-
}, [shownPythonToolboxCategories, i18n.language]);
272-
273-
// Add event listener for toolbox updates
274-
React.useEffect(() => {
275-
window.addEventListener(TOOLBOX_UPDATE_EVENT, handleToolboxUpdateRequest);
276-
277-
return () => {
278-
window.removeEventListener(TOOLBOX_UPDATE_EVENT, handleToolboxUpdateRequest);
279-
};
280-
}, [handleToolboxUpdateRequest]);
281-
282262
// Initialize blocks when app loads
283263
React.useEffect(() => {
284264
initializeBlocks();
@@ -519,8 +499,8 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
519499
storage={storage}
520500
setAlertErrorMessage={setAlertErrorMessage}
521501
gotoTab={setActiveTab}
522-
project={project}
523-
setProject={setProject}
502+
currentProject={project}
503+
setCurrentProject={setProject}
524504
onProjectChanged={onProjectChanged}
525505
openWPIToolboxSettings={() => setToolboxSettingsModalIsOpen(true)}
526506
theme={theme}

src/blocks/mrc_call_python_function.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ type FunctionArg = {
7070

7171
const WARNING_ID_FUNCTION_CHANGED = 'function changed';
7272

73-
export type CallPythonFunctionBlock = Blockly.Block & CallPythonFunctionMixin & Blockly.BlockSvg;
73+
export type CallPythonFunctionBlock = Blockly.Block & CallPythonFunctionMixin;
7474
interface CallPythonFunctionMixin extends CallPythonFunctionMixinType {
7575
mrcFunctionKind: FunctionKind,
7676
mrcReturnType: string,
@@ -871,7 +871,9 @@ const CALL_PYTHON_FUNCTION = {
871871
if (icon) {
872872
icon.setBubbleVisible(true);
873873
}
874-
this.bringToFront();
874+
if (this.rendered) {
875+
(this as unknown as Blockly.BlockSvg).bringToFront();
876+
}
875877
} else {
876878
// Clear the existing warning on the block.
877879
this.setWarningText(null, WARNING_ID_FUNCTION_CHANGED);

src/blocks/mrc_class_method_def.ts

Lines changed: 32 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ import * as toolboxItems from '../toolbox/items';
3535
import { getClassData } from './utils/python';
3636
import { FunctionData } from './utils/python_json_types';
3737
import { findConnectedBlocksOfType } from './utils/find_connected_blocks';
38+
import { makeLegalName } from './utils/validator';
39+
import { NONCOPYABLE_BLOCK } from './noncopyable_block';
3840
import { BLOCK_NAME as MRC_GET_PARAMETER_BLOCK_NAME } from './mrc_get_parameter';
3941
import * as paramContainer from './mrc_param_container'
4042

@@ -57,7 +59,7 @@ export interface Parameter {
5759
type?: string;
5860
}
5961

60-
export type ClassMethodDefBlock = Blockly.Block & ClassMethodDefMixin & Blockly.BlockSvg;
62+
export type ClassMethodDefBlock = Blockly.Block & ClassMethodDefMixin;
6163
interface ClassMethodDefMixin extends ClassMethodDefMixinType {
6264
mrcMethodId: string,
6365
mrcCanChangeSignature: boolean,
@@ -122,6 +124,7 @@ const CLASS_METHOD_DEF = {
122124
this.setNextStatement(false);
123125
this.updateBlock_();
124126
},
127+
...NONCOPYABLE_BLOCK,
125128
/**
126129
* Returns the state of this block as a JSON serializable object.
127130
*/
@@ -180,12 +183,18 @@ const CLASS_METHOD_DEF = {
180183
if (this.mrcCanChangeSignature) {
181184
const nameField = new Blockly.FieldTextInput(name);
182185
input.insertFieldAt(0, nameField, FIELD_METHOD_NAME);
183-
this.setMutator(paramContainer.getMutatorIcon(this));
186+
if (this.rendered) {
187+
this.setMutator(paramContainer.getMutatorIcon(this as unknown as Blockly.BlockSvg));
188+
}
184189
nameField.setValidator(this.mrcNameFieldValidator.bind(this, nameField));
185190
} else {
186191
input.insertFieldAt(0, createFieldNonEditableText(name), FIELD_METHOD_NAME);
187-
// Case because a current bug in blockly where it won't allow passing null to Blockly.Block.setMutator makes it necessary.
188-
(this as Blockly.BlockSvg).setMutator(null);
192+
// Block.setMutator is defined as setMutator(_mutator: MutatorIcon) and BlockSvg.setMutator
193+
// is defined as setMutator(mutator: MutatorIcon | null).
194+
// Therefore, to call setMutator(null), this must be casted to BlockSvg.
195+
if (this.rendered) {
196+
(this as unknown as Blockly.BlockSvg).setMutator(null);
197+
}
189198
}
190199
this.mrcUpdateParams();
191200
this.mrcUpdateReturnInput();
@@ -229,7 +238,9 @@ const CLASS_METHOD_DEF = {
229238
* mrcOnMutatorOpen is called when the mutator on a ClassMethodDefBlock is opened.
230239
*/
231240
mrcOnMutatorOpen: function(this: ClassMethodDefBlock): void {
232-
paramContainer.onMutatorOpen(this);
241+
if (this.rendered) {
242+
paramContainer.onMutatorOpen(this as unknown as Blockly.BlockSvg);
243+
}
233244
},
234245
mrcRenameParameter: function (this: ClassMethodDefBlock, oldName: string, newName: string) {
235246
const nextBlock = this.getInputTargetBlock(INPUT_STACK);
@@ -283,10 +294,23 @@ const CLASS_METHOD_DEF = {
283294
// When the user changes the method name on the block, clear the mrcPythonMethodName field.
284295
this.mrcPythonMethodName = '';
285296

286-
// Strip leading and trailing whitespace.
287-
name = name.trim();
297+
if (this.isInFlyout) {
298+
// Flyouts can have multiple methods with identical names.
299+
return name;
300+
}
301+
302+
const otherNames: string[] = [];
303+
this.workspace.getBlocksByType(BLOCK_NAME)
304+
.filter(block => block.id !== this.id)
305+
.forEach((block) => {
306+
otherNames.push(block.getFieldValue(FIELD_METHOD_NAME));
307+
const classMethodDefBlock = block as ClassMethodDefBlock;
308+
if (classMethodDefBlock.mrcPythonMethodName) {
309+
otherNames.push(classMethodDefBlock.mrcPythonMethodName);
310+
}
311+
});
288312

289-
const legalName = findLegalMethodName(name, this);
313+
const legalName = makeLegalName(name, otherNames, /* mustBeValidPythonIdentifier */ true);
290314
const oldName = nameField.getValue();
291315
if (oldName && oldName !== name && oldName !== legalName) {
292316
// Rename any callers.
@@ -364,61 +388,6 @@ const CLASS_METHOD_DEF = {
364388
},
365389
};
366390

367-
/**
368-
* Ensure two identically-named methods don't exist.
369-
* Take the proposed method name, and return a legal name i.e. one that
370-
* is not empty and doesn't collide with other methods.
371-
*
372-
* @param name Proposed method name.
373-
* @param block Block to disambiguate.
374-
* @returns Non-colliding name.
375-
*/
376-
function findLegalMethodName(name: string, block: ClassMethodDefBlock): string {
377-
if (block.isInFlyout) {
378-
// Flyouts can have multiple methods called 'my_method'.
379-
return name;
380-
}
381-
name = name || 'unnamed';
382-
while (isMethodNameUsed(name, block.workspace, block)) {
383-
// Collision with another method.
384-
const r = name.match(/^(.*?)(\d+)$/);
385-
if (!r) {
386-
name += '2';
387-
} else {
388-
name = r[1] + (parseInt(r[2]) + 1);
389-
}
390-
}
391-
return name;
392-
}
393-
394-
/**
395-
* Return if the given name is already a method name.
396-
*
397-
* @param name The questionable name.
398-
* @param workspace The workspace to scan for collisions.
399-
* @param opt_exclude Optional block to exclude from comparisons (one doesn't
400-
* want to collide with oneself).
401-
* @returns True if the name is used, otherwise return false.
402-
*/
403-
function isMethodNameUsed(
404-
name: string, workspace: Blockly.Workspace, opt_exclude?: Blockly.Block): boolean {
405-
const nameLowerCase = name.toLowerCase();
406-
for (const block of workspace.getBlocksByType(BLOCK_NAME)) {
407-
if (block === opt_exclude) {
408-
continue;
409-
}
410-
if (nameLowerCase === block.getFieldValue(FIELD_METHOD_NAME).toLowerCase()) {
411-
return true;
412-
}
413-
const classMethodDefBlock = block as ClassMethodDefBlock;
414-
if (classMethodDefBlock.mrcPythonMethodName &&
415-
nameLowerCase === classMethodDefBlock.mrcPythonMethodName.toLowerCase()) {
416-
return true;
417-
}
418-
}
419-
return false;
420-
}
421-
422391
export const setup = function () {
423392
Blockly.Blocks[BLOCK_NAME] = CLASS_METHOD_DEF;
424393
};

src/blocks/mrc_component.ts

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* @license
33
* Copyright 2025 Porpoiseful LLC
4-
*
4+
*
55
* Licensed under the Apache License, Version 2.0 (the "License");
66
* you may not use this file except in compliance with the License.
77
* You may obtain a copy of the License at
@@ -28,9 +28,11 @@ import { Editor } from '../editor/editor';
2828
import { ExtendedPythonGenerator } from '../editor/extended_python_generator';
2929
import { getModuleTypeForWorkspace } from './utils/workspaces';
3030
import { getAllowedTypesForSetCheck, getClassData, getSubclassNames } from './utils/python';
31+
import { makeLegalName } from './utils/validator';
3132
import * as toolboxItems from '../toolbox/items';
3233
import * as storageModule from '../storage/module';
3334
import * as storageModuleContent from '../storage/module_content';
35+
import { NONCOPYABLE_BLOCK } from './noncopyable_block';
3436
import {
3537
BLOCK_NAME as MRC_MECHANISM_COMPONENT_HOLDER,
3638
MechanismComponentHolderBlock,
@@ -100,6 +102,7 @@ const COMPONENT = {
100102
this.setPreviousStatement(true, OUTPUT_NAME);
101103
this.setNextStatement(true, OUTPUT_NAME);
102104
},
105+
...NONCOPYABLE_BLOCK,
103106

104107
/**
105108
* Returns the state of this block as a JSON serializable object.
@@ -162,14 +165,27 @@ const COMPONENT = {
162165
}
163166
},
164167
mrcNameFieldValidator(this: ComponentBlock, nameField: Blockly.FieldTextInput, name: string): string {
165-
// Strip leading and trailing whitespace.
166-
name = name.trim();
168+
if (this.isInFlyout) {
169+
// Flyouts can have multiple methods with identical names.
170+
return name;
171+
}
172+
173+
const otherNames: string[] = [];
174+
this.workspace.getBlocksByType(BLOCK_NAME)
175+
.filter(block => block.id !== this.id)
176+
.forEach((block) => {
177+
otherNames.push(block.getFieldValue(FIELD_NAME));
178+
});
167179

168-
const legalName = name;
180+
const legalName = makeLegalName(name, otherNames, /* mustBeValidPythonIdentifier */ true);
169181
const oldName = nameField.getValue();
170182
if (oldName && oldName !== name && oldName !== legalName) {
171183
// Rename any callers.
172184
renameMethodCallers(this.workspace, this.mrcComponentId, legalName);
185+
const editor = Editor.getEditorForBlocklyWorkspace(this.workspace);
186+
if (editor) {
187+
editor.updateToolboxAfterDelay();
188+
}
173189
}
174190
return legalName;
175191
},
@@ -192,7 +208,7 @@ const COMPONENT = {
192208
// Collect the ports for this component block.
193209
for (let i = 0; i < this.mrcArgs.length; i++) {
194210
const argName = this.getArgName(i);
195-
ports[argName] = this.mrcArgs[i].name;
211+
ports[argName] = this.mrcArgs[i].name;
196212
}
197213
},
198214
/**
@@ -210,15 +226,15 @@ const COMPONENT = {
210226
/**
211227
* mrcOnMove is called when a ComponentBlock is moved.
212228
*/
213-
mrcOnMove: function(this: ComponentBlock, reason: string[]): void {
229+
mrcOnMove: function(this: ComponentBlock, editor: Editor, reason: string[]): void {
214230
this.checkBlockIsInHolder();
215231
if (reason.includes('connect')) {
216232
const rootBlock: Blockly.Block | null = this.getRootBlock();
217233
if (rootBlock && rootBlock.type === MRC_MECHANISM_COMPONENT_HOLDER) {
218234
(rootBlock as MechanismComponentHolderBlock).setNameOfChildBlock(this);
219235
}
220236
}
221-
mrcDescendantsMayHaveChanged(this.workspace);
237+
mrcDescendantsMayHaveChanged(this.workspace, editor);
222238
},
223239
checkBlockIsInHolder: function(this: ComponentBlock): void {
224240
const rootBlock: Blockly.Block | null = this.getRootBlock();
@@ -247,6 +263,13 @@ const COMPONENT = {
247263
this.mrcComponentId = oldIdToNewId[this.mrcComponentId];
248264
}
249265
},
266+
upgrade_005_to_006: function(this: ComponentBlock) {
267+
for (let i = 0; i < this.mrcArgs.length; i++) {
268+
if (this.mrcArgs[i].type === 'Port') {
269+
this.mrcArgs[i].type = this.mrcArgs[i].name;
270+
}
271+
}
272+
},
250273
};
251274

252275
export const setup = function () {
@@ -318,11 +341,21 @@ function createComponentBlock(
318341
if (constructorData.expectedPortType) {
319342
extraState.params!.push({
320343
name: constructorData.expectedPortType,
321-
type: 'Port',
344+
type: constructorData.expectedPortType,
322345
});
323-
if ( moduleType == storageModule.ModuleType.ROBOT ) {
346+
if (moduleType == storageModule.ModuleType.ROBOT ) {
324347
inputs['ARG0'] = createPort(constructorData.expectedPortType);
325348
}
326349
}
327350
return new toolboxItems.Block(BLOCK_NAME, extraState, fields, Object.keys(inputs).length ? inputs : null);
328351
}
352+
353+
/**
354+
* Upgrades the ComponentBlocks in the given workspace from version 005 to 006.
355+
* This function should only be called when upgrading old projects.
356+
*/
357+
export function upgrade_005_to_006(workspace: Blockly.Workspace): void {
358+
workspace.getBlocksByType(BLOCK_NAME).forEach(block => {
359+
(block as ComponentBlock).upgrade_005_to_006();
360+
});
361+
}

0 commit comments

Comments
 (0)