chore: Merge branch 'develop' into rc/v12.0.0

This commit is contained in:
Christopher Allen
2025-05-03 02:00:27 +01:00
10 changed files with 720 additions and 446 deletions

View File

@@ -7,6 +7,9 @@ name: conventional-release-labels
jobs: jobs:
label: label:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps: steps:
- uses: bcoe/conventional-release-labels@v1 - uses: bcoe/conventional-release-labels@v1
with: with:

View File

@@ -521,6 +521,21 @@ export class BlockSvg
this.updateCollapsed(); this.updateCollapsed();
} }
/**
* Traverses child blocks to see if any of them have a warning.
*
* @returns true if any child has a warning, false otherwise.
*/
private childHasWarning(): boolean {
const children = this.getChildren(false);
for (const child of children) {
if (child.getIcon(WarningIcon.TYPE) || child.childHasWarning()) {
return true;
}
}
return false;
}
/** /**
* Makes sure that when the block is collapsed, it is rendered correctly * Makes sure that when the block is collapsed, it is rendered correctly
* for that state. * for that state.
@@ -544,10 +559,17 @@ export class BlockSvg
this.updateDisabled(); this.updateDisabled();
this.removeInput(collapsedInputName); this.removeInput(collapsedInputName);
dom.removeClass(this.svgGroup, 'blocklyCollapsed'); dom.removeClass(this.svgGroup, 'blocklyCollapsed');
this.setWarningText(null, BlockSvg.COLLAPSED_WARNING_ID);
return; return;
} }
dom.addClass(this.svgGroup, 'blocklyCollapsed'); dom.addClass(this.svgGroup, 'blocklyCollapsed');
if (this.childHasWarning()) {
this.setWarningText(
Msg['COLLAPSED_WARNINGS_WARNING'],
BlockSvg.COLLAPSED_WARNING_ID,
);
}
const text = this.toString(internalConstants.COLLAPSE_CHARS); const text = this.toString(internalConstants.COLLAPSE_CHARS);
const field = this.getField(collapsedFieldName); const field = this.getField(collapsedFieldName);

View File

@@ -129,26 +129,11 @@ export class FieldDropdown extends Field<string> {
// If we pass SKIP_SETUP, don't do *anything* with the menu generator. // If we pass SKIP_SETUP, don't do *anything* with the menu generator.
if (menuGenerator === Field.SKIP_SETUP) return; if (menuGenerator === Field.SKIP_SETUP) return;
if (Array.isArray(menuGenerator)) { this.setOptions(menuGenerator);
this.validateOptions(menuGenerator);
const trimmed = this.trimOptions(menuGenerator);
this.menuGenerator_ = trimmed.options;
this.prefixField = trimmed.prefix || null;
this.suffixField = trimmed.suffix || null;
} else {
this.menuGenerator_ = menuGenerator;
}
/**
* The currently selected option. The field is initialized with the
* first option selected.
*/
this.selectedOption = this.getOptions(false)[0];
if (config) { if (config) {
this.configure_(config); this.configure_(config);
} }
this.setValue(this.selectedOption[1]);
if (validator) { if (validator) {
this.setValidator(validator); this.setValidator(validator);
} }
@@ -417,6 +402,28 @@ export class FieldDropdown extends Field<string> {
return this.generatedOptions; return this.generatedOptions;
} }
/**
* Update the options on this dropdown. This will reset the selected item to
* the first item in the list.
*
* @param menuGenerator The array of options or a generator function.
*/
setOptions(menuGenerator: MenuGenerator) {
if (Array.isArray(menuGenerator)) {
this.validateOptions(menuGenerator);
const trimmed = this.trimOptions(menuGenerator);
this.menuGenerator_ = trimmed.options;
this.prefixField = trimmed.prefix || null;
this.suffixField = trimmed.suffix || null;
} else {
this.menuGenerator_ = menuGenerator;
}
// The currently selected option. The field is initialized with the
// first option selected.
this.selectedOption = this.getOptions(false)[0];
this.setValue(this.selectedOption[1]);
}
/** /**
* Ensure that the input value is a valid language-neutral option. * Ensure that the input value is a valid language-neutral option.
* *

View File

@@ -13,7 +13,6 @@
// Unused import preserved for side-effects. Remove if unneeded. // Unused import preserved for side-effects. Remove if unneeded.
import {BlockSvg} from '../block_svg.js'; import {BlockSvg} from '../block_svg.js';
import type {BlocklyOptions} from '../blockly_options.js';
import * as browserEvents from '../browser_events.js'; import * as browserEvents from '../browser_events.js';
import * as common from '../common.js'; import * as common from '../common.js';
import {ComponentManager} from '../component_manager.js'; import {ComponentManager} from '../component_manager.js';
@@ -36,7 +35,6 @@ import {isSelectableToolboxItem} from '../interfaces/i_selectable_toolbox_item.j
import type {IStyleable} from '../interfaces/i_styleable.js'; import type {IStyleable} from '../interfaces/i_styleable.js';
import type {IToolbox} from '../interfaces/i_toolbox.js'; import type {IToolbox} from '../interfaces/i_toolbox.js';
import type {IToolboxItem} from '../interfaces/i_toolbox_item.js'; import type {IToolboxItem} from '../interfaces/i_toolbox_item.js';
import {Options} from '../options.js';
import * as registry from '../registry.js'; import * as registry from '../registry.js';
import type {KeyboardShortcut} from '../shortcut_registry.js'; import type {KeyboardShortcut} from '../shortcut_registry.js';
import * as Touch from '../touch.js'; import * as Touch from '../touch.js';
@@ -333,18 +331,7 @@ export class Toolbox
*/ */
protected createFlyout_(): IFlyout { protected createFlyout_(): IFlyout {
const workspace = this.workspace_; const workspace = this.workspace_;
// TODO (#4247): Look into adding a makeFlyout method to Blockly Options. const workspaceOptions = workspace.copyOptionsForFlyout();
const workspaceOptions = new Options({
'parentWorkspace': workspace,
'rtl': workspace.RTL,
'oneBasedIndex': workspace.options.oneBasedIndex,
'horizontalLayout': workspace.horizontalLayout,
'renderer': workspace.options.renderer,
'rendererOverrides': workspace.options.rendererOverrides,
'move': {
'scrollbars': true,
},
} as BlocklyOptions);
// Options takes in either 'end' or 'start'. This has already been parsed to // Options takes in either 'end' or 'start'. This has already been parsed to
// be either 0 or 1, so set it after. // be either 0 or 1, so set it after.
workspaceOptions.toolboxPosition = workspace.options.toolboxPosition; workspaceOptions.toolboxPosition = workspace.options.toolboxPosition;

View File

@@ -12,7 +12,6 @@
// Former goog.module ID: Blockly.Trashcan // Former goog.module ID: Blockly.Trashcan
// Unused import preserved for side-effects. Remove if unneeded. // Unused import preserved for side-effects. Remove if unneeded.
import type {BlocklyOptions} from './blockly_options.js';
import * as browserEvents from './browser_events.js'; import * as browserEvents from './browser_events.js';
import {ComponentManager} from './component_manager.js'; import {ComponentManager} from './component_manager.js';
import {DeleteArea} from './delete_area.js'; import {DeleteArea} from './delete_area.js';
@@ -26,7 +25,6 @@ import type {IDraggable} from './interfaces/i_draggable.js';
import type {IFlyout} from './interfaces/i_flyout.js'; import type {IFlyout} from './interfaces/i_flyout.js';
import type {IPositionable} from './interfaces/i_positionable.js'; import type {IPositionable} from './interfaces/i_positionable.js';
import type {UiMetrics} from './metrics_manager.js'; import type {UiMetrics} from './metrics_manager.js';
import {Options} from './options.js';
import * as uiPosition from './positionable_helpers.js'; import * as uiPosition from './positionable_helpers.js';
import * as registry from './registry.js'; import * as registry from './registry.js';
import type * as blocks from './serialization/blocks.js'; import type * as blocks from './serialization/blocks.js';
@@ -103,17 +101,7 @@ export class Trashcan
} }
// Create flyout options. // Create flyout options.
const flyoutWorkspaceOptions = new Options({ const flyoutWorkspaceOptions = this.workspace.copyOptionsForFlyout();
'scrollbars': true,
'parentWorkspace': this.workspace,
'rtl': this.workspace.RTL,
'oneBasedIndex': this.workspace.options.oneBasedIndex,
'renderer': this.workspace.options.renderer,
'rendererOverrides': this.workspace.options.rendererOverrides,
'move': {
'scrollbars': true,
},
} as BlocklyOptions);
// Create vertical or horizontal flyout. // Create vertical or horizontal flyout.
if (this.workspace.horizontalLayout) { if (this.workspace.horizontalLayout) {
flyoutWorkspaceOptions.toolboxPosition = flyoutWorkspaceOptions.toolboxPosition =

View File

@@ -985,6 +985,28 @@ export class WorkspaceSvg
this.svgGroup_.appendChild(svgZoomControls); this.svgGroup_.appendChild(svgZoomControls);
} }
/**
* Creates a new set of options from this workspace's options with just the
* values that are relevant to a flyout.
*
* @returns A subset of this workspace's options.
*/
copyOptionsForFlyout(): Options {
return new Options({
'parentWorkspace': this,
'rtl': this.RTL,
'oneBasedIndex': this.options.oneBasedIndex,
'horizontalLayout': this.horizontalLayout,
'renderer': this.options.renderer,
'rendererOverrides': this.options.rendererOverrides,
'plugins': this.options.plugins,
'modalInputs': this.options.modalInputs,
'move': {
'scrollbars': true,
},
} as BlocklyOptions);
}
/** /**
* Add a flyout element in an element with the given tag name. * Add a flyout element in an element with the given tag name.
* *
@@ -993,17 +1015,7 @@ export class WorkspaceSvg
* @internal * @internal
*/ */
addFlyout(tagName: string | Svg<SVGSVGElement> | Svg<SVGGElement>): Element { addFlyout(tagName: string | Svg<SVGSVGElement> | Svg<SVGGElement>): Element {
const workspaceOptions = new Options({ const workspaceOptions = this.copyOptionsForFlyout();
'parentWorkspace': this,
'rtl': this.RTL,
'oneBasedIndex': this.options.oneBasedIndex,
'horizontalLayout': this.horizontalLayout,
'renderer': this.options.renderer,
'rendererOverrides': this.options.rendererOverrides,
'move': {
'scrollbars': true,
},
} as BlocklyOptions);
workspaceOptions.toolboxPosition = this.options.toolboxPosition; workspaceOptions.toolboxPosition = this.options.toolboxPosition;
if (this.horizontalLayout) { if (this.horizontalLayout) {
const HorizontalFlyout = registry.getClassFromOptions( const HorizontalFlyout = registry.getClassFromOptions(

931
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -112,11 +112,11 @@
"concurrently": "^9.0.1", "concurrently": "^9.0.1",
"eslint": "^9.15.0", "eslint": "^9.15.0",
"eslint-config-google": "^0.14.0", "eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^10.1.1",
"eslint-plugin-jsdoc": "^50.5.0", "eslint-plugin-jsdoc": "^50.5.0",
"eslint-plugin-prettier": "^5.2.1", "eslint-plugin-prettier": "^5.2.1",
"glob": "^11.0.1", "glob": "^11.0.1",
"globals": "^15.12.0", "globals": "^16.0.0",
"google-closure-compiler": "^20240317.0.0", "google-closure-compiler": "^20240317.0.0",
"gulp": "^5.0.0", "gulp": "^5.0.0",
"gulp-concat": "^2.6.1", "gulp-concat": "^2.6.1",
@@ -144,7 +144,7 @@
"yargs": "^17.2.1" "yargs": "^17.2.1"
}, },
"dependencies": { "dependencies": {
"jsdom": "25.0.1" "jsdom": "26.1.0"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"

View File

@@ -1881,6 +1881,62 @@ suite('Blocks', function () {
}); });
}); });
suite('Warning icons and collapsing', function () {
setup(function () {
this.workspace = Blockly.inject('blocklyDiv');
this.parentBlock = Blockly.serialization.blocks.append(
{
'type': 'statement_block',
'inputs': {
'STATEMENT': {
'block': {
'type': 'statement_block',
},
},
},
},
this.workspace,
);
this.parentBlock.initSvg();
this.parentBlock.render();
this.childBlock = this.parentBlock.getInputTargetBlock('STATEMENT');
this.childBlock.initSvg();
this.childBlock.render();
});
teardown(function () {
workspaceTeardown.call(this, this.workspace);
});
test('Adding a warning to a child block does not affect the parent', function () {
const text = 'Warning Text';
this.childBlock.setWarningText(text);
const icon = this.parentBlock.getIcon(Blockly.icons.WarningIcon.TYPE);
assert.isUndefined(
icon,
"Setting a child block's warning should not add a warning to the parent",
);
});
test('Warnings are added and removed when collapsing a stack with warnings', function () {
const text = 'Warning Text';
this.childBlock.setWarningText(text);
this.parentBlock.setCollapsed(true);
let icon = this.parentBlock.getIcon(Blockly.icons.WarningIcon.TYPE);
assert.exists(icon?.getText(), 'Expected warning icon text to be set');
this.parentBlock.setCollapsed(false);
icon = this.parentBlock.getIcon(Blockly.icons.WarningIcon.TYPE);
assert.isUndefined(
icon,
'Warning should be removed from parent after expanding',
);
});
});
suite('Bubbles and collapsing', function () { suite('Bubbles and collapsing', function () {
setup(function () { setup(function () {
this.workspace = Blockly.inject('blocklyDiv'); this.workspace = Blockly.inject('blocklyDiv');

View File

@@ -195,6 +195,52 @@ suite('Dropdown Fields', function () {
assertFieldValue(this.field, 'B', 'b'); assertFieldValue(this.field, 'B', 'b');
}); });
}); });
suite('setOptions', function () {
setup(function () {
this.field = new Blockly.FieldDropdown([
['a', 'A'],
['b', 'B'],
['c', 'C'],
]);
});
test('With array updates options', function () {
this.field.setOptions([
['d', 'D'],
['e', 'E'],
['f', 'F'],
]);
assertFieldValue(this.field, 'D', 'd');
});
test('With generator updates options', function () {
this.field.setOptions(function () {
return [
['d', 'D'],
['e', 'E'],
['f', 'F'],
];
});
assertFieldValue(this.field, 'D', 'd');
});
test('With trimmable options gets trimmed', function () {
this.field.setOptions([
['a d b', 'D'],
['a e b', 'E'],
['a f b', 'F'],
]);
assert.deepEqual(this.field.prefixField, 'a');
assert.deepEqual(this.field.suffixField, 'b');
assert.deepEqual(this.field.getOptions(), [
['d', 'D'],
['e', 'E'],
['f', 'F'],
]);
});
test('With an empty array of options throws', function () {
assert.throws(function () {
this.field.setOptions([]);
});
});
});
suite('Validators', function () { suite('Validators', function () {
setup(function () { setup(function () {