From d5f3d157264fceb173befcd3ea985e9a05569f9c Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Fri, 11 Jul 2025 10:54:19 -0700 Subject: [PATCH 01/38] feat: Add support for keyboard navigation to/from block comments. (#9227) * refactor: Update `TextInputBubble` to use `CommentEditor` for text editing. * feat: Designate `Bubble` as implementing `IFocusableNode`. * feat: Dismiss focused bubbles on Escape. * feat: Add support for keyboard navigation to block comments. * fix: Scroll comment editors rather than zooming the workspace. * chore: Add param to docstring. --- core/bubbles/bubble.ts | 33 ++++- core/bubbles/textinput_bubble.ts | 120 +++++------------- core/comments/comment_editor.ts | 6 + core/comments/rendered_workspace_comment.ts | 9 -- core/icons/comment_icon.ts | 6 +- .../block_comment_navigation_policy.ts | 76 +++++++++++ .../comment_editor_navigation_policy.ts | 54 ++++++++ core/keyboard_nav/icon_navigation_policy.ts | 7 + core/navigator.ts | 4 + core/workspace_svg.ts | 6 + 10 files changed, 221 insertions(+), 100 deletions(-) create mode 100644 core/keyboard_nav/block_comment_navigation_policy.ts create mode 100644 core/keyboard_nav/comment_editor_navigation_policy.ts diff --git a/core/bubbles/bubble.ts b/core/bubbles/bubble.ts index 20e730abb..c42e60254 100644 --- a/core/bubbles/bubble.ts +++ b/core/bubbles/bubble.ts @@ -9,7 +9,9 @@ import * as common from '../common.js'; import {BubbleDragStrategy} from '../dragging/bubble_drag_strategy.js'; import {getFocusManager} from '../focus_manager.js'; import {IBubble} from '../interfaces/i_bubble.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; import type {IFocusableTree} from '../interfaces/i_focusable_tree.js'; +import type {IHasBubble} from '../interfaces/i_has_bubble.js'; import {ISelectable} from '../interfaces/i_selectable.js'; import {ContainerRegion} from '../metrics_manager.js'; import {Scrollbar} from '../scrollbar.js'; @@ -27,7 +29,7 @@ import {WorkspaceSvg} from '../workspace_svg.js'; * bubble, where it has a "tail" that points to the block, and a "head" that * displays arbitrary svg elements. */ -export abstract class Bubble implements IBubble, ISelectable { +export abstract class Bubble implements IBubble, ISelectable, IFocusableNode { /** The width of the border around the bubble. */ static readonly BORDER_WIDTH = 6; @@ -100,12 +102,14 @@ export abstract class Bubble implements IBubble, ISelectable { * element that's represented by this bubble (as a focusable node). This * element will have its ID overwritten. If not provided, the focusable * element of this node will default to the bubble's SVG root. + * @param owner The object responsible for hosting/spawning this bubble. */ constructor( public readonly workspace: WorkspaceSvg, protected anchor: Coordinate, protected ownerRect?: Rect, overriddenFocusableElement?: SVGElement | HTMLElement, + protected owner?: IHasBubble & IFocusableNode, ) { this.id = idGenerator.getNextUniqueId(); this.svgRoot = dom.createSvgElement( @@ -145,6 +149,13 @@ export abstract class Bubble implements IBubble, ISelectable { this, this.onMouseDown, ); + + browserEvents.conditionalBind( + this.focusableElement, + 'keydown', + this, + this.onKeyDown, + ); } /** Dispose of this bubble. */ @@ -229,6 +240,19 @@ export abstract class Bubble implements IBubble, ISelectable { getFocusManager().focusNode(this); } + /** + * Handles key events when this bubble is focused. By default, closes the + * bubble on Escape. + * + * @param e The keyboard event to handle. + */ + protected onKeyDown(e: KeyboardEvent) { + if (e.key === 'Escape' && this.owner) { + this.owner.setBubbleVisible(false); + getFocusManager().focusNode(this.owner); + } + } + /** Positions the bubble relative to its anchor. Does not render its tail. */ protected positionRelativeToAnchor() { let left = this.anchor.x; @@ -694,4 +718,11 @@ export abstract class Bubble implements IBubble, ISelectable { canBeFocused(): boolean { return true; } + + /** + * Returns the object that owns/hosts this bubble, if any. + */ + getOwner(): (IHasBubble & IFocusableNode) | undefined { + return this.owner; + } } diff --git a/core/bubbles/textinput_bubble.ts b/core/bubbles/textinput_bubble.ts index 7479c06cf..0bad5fabc 100644 --- a/core/bubbles/textinput_bubble.ts +++ b/core/bubbles/textinput_bubble.ts @@ -4,7 +4,11 @@ * SPDX-License-Identifier: Apache-2.0 */ +import {CommentEditor} from '../comments/comment_editor.js'; import * as Css from '../css.js'; +import {getFocusManager} from '../focus_manager.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; +import type {IHasBubble} from '../interfaces/i_has_bubble.js'; import * as touch from '../touch.js'; import {browserEvents} from '../utils.js'; import {Coordinate} from '../utils/coordinate.js'; @@ -21,12 +25,6 @@ import {Bubble} from './bubble.js'; * Used by the comment icon. */ export class TextInputBubble extends Bubble { - /** The root of the elements specific to the text element. */ - private inputRoot: SVGForeignObjectElement; - - /** The text input area element. */ - private textArea: HTMLTextAreaElement; - /** The group containing the lines indicating the bubble is resizable. */ private resizeGroup: SVGGElement; @@ -42,18 +40,12 @@ export class TextInputBubble extends Bubble { */ private resizePointerMoveListener: browserEvents.Data | null = null; - /** Functions listening for changes to the text of this bubble. */ - private textChangeListeners: (() => void)[] = []; - /** Functions listening for changes to the size of this bubble. */ private sizeChangeListeners: (() => void)[] = []; /** Functions listening for changes to the location of this bubble. */ private locationChangeListeners: (() => void)[] = []; - /** The text of this bubble. */ - private text = ''; - /** The default size of this bubble, including borders. */ private readonly DEFAULT_SIZE = new Size( 160 + Bubble.DOUBLE_BORDER, @@ -68,46 +60,47 @@ export class TextInputBubble extends Bubble { private editable = true; + /** View responsible for supporting text editing. */ + private editor: CommentEditor; + /** * @param workspace The workspace this bubble belongs to. * @param anchor The anchor location of the thing this bubble is attached to. * The tail of the bubble will point to this location. * @param ownerRect An optional rect we don't want the bubble to overlap with * when automatically positioning. + * @param owner The object that owns/hosts this bubble. */ constructor( public readonly workspace: WorkspaceSvg, protected anchor: Coordinate, protected ownerRect?: Rect, + protected owner?: IHasBubble & IFocusableNode, ) { - super(workspace, anchor, ownerRect, TextInputBubble.createTextArea()); + super(workspace, anchor, ownerRect, undefined, owner); dom.addClass(this.svgRoot, 'blocklyTextInputBubble'); - this.textArea = this.getFocusableElement() as HTMLTextAreaElement; - this.inputRoot = this.createEditor(this.contentContainer, this.textArea); + this.editor = new CommentEditor(workspace, this.id, () => { + getFocusManager().focusNode(this); + }); + this.contentContainer.appendChild(this.editor.getDom()); this.resizeGroup = this.createResizeHandle(this.svgRoot, workspace); this.setSize(this.DEFAULT_SIZE, true); } /** @returns the text of this bubble. */ getText(): string { - return this.text; + return this.editor.getText(); } /** Sets the text of this bubble. Calls change listeners. */ setText(text: string) { - this.text = text; - this.textArea.value = text; - this.onTextChange(); + this.editor.setText(text); } /** Sets whether or not the text in the bubble is editable. */ setEditable(editable: boolean) { this.editable = editable; - if (this.editable) { - this.textArea.removeAttribute('readonly'); - } else { - this.textArea.setAttribute('readonly', ''); - } + this.editor.setEditable(editable); } /** Returns whether or not the text in the bubble is editable. */ @@ -117,7 +110,7 @@ export class TextInputBubble extends Bubble { /** Adds a change listener to be notified when this bubble's text changes. */ addTextChangeListener(listener: () => void) { - this.textChangeListeners.push(listener); + this.editor.addTextChangeListener(listener); } /** Adds a change listener to be notified when this bubble's size changes. */ @@ -130,58 +123,6 @@ export class TextInputBubble extends Bubble { this.locationChangeListeners.push(listener); } - /** Creates and returns the editable text area for this bubble's editor. */ - private static createTextArea(): HTMLTextAreaElement { - const textArea = document.createElementNS( - dom.HTML_NS, - 'textarea', - ) as HTMLTextAreaElement; - textArea.className = 'blocklyTextarea blocklyText'; - return textArea; - } - - /** Creates and returns the UI container element for this bubble's editor. */ - private createEditor( - container: SVGGElement, - textArea: HTMLTextAreaElement, - ): SVGForeignObjectElement { - const inputRoot = dom.createSvgElement( - Svg.FOREIGNOBJECT, - { - 'x': Bubble.BORDER_WIDTH, - 'y': Bubble.BORDER_WIDTH, - }, - container, - ); - - const body = document.createElementNS(dom.HTML_NS, 'body'); - body.setAttribute('xmlns', dom.HTML_NS); - body.className = 'blocklyMinimalBody'; - - textArea.setAttribute('dir', this.workspace.RTL ? 'RTL' : 'LTR'); - body.appendChild(textArea); - inputRoot.appendChild(body); - - this.bindTextAreaEvents(textArea); - - return inputRoot; - } - - /** Binds events to the text area element. */ - private bindTextAreaEvents(textArea: HTMLTextAreaElement) { - // Don't zoom with mousewheel; let it scroll instead. - browserEvents.conditionalBind(textArea, 'wheel', this, (e: Event) => { - e.stopPropagation(); - }); - // Don't let the pointerdown event get to the workspace. - browserEvents.conditionalBind(textArea, 'pointerdown', this, (e: Event) => { - e.stopPropagation(); - touch.clearTouchIdentifier(); - }); - - browserEvents.conditionalBind(textArea, 'change', this, this.onTextChange); - } - /** Creates the resize handler elements and binds events to them. */ private createResizeHandle( container: SVGGElement, @@ -220,8 +161,12 @@ export class TextInputBubble extends Bubble { const widthMinusBorder = size.width - Bubble.DOUBLE_BORDER; const heightMinusBorder = size.height - Bubble.DOUBLE_BORDER; - this.inputRoot.setAttribute('width', `${widthMinusBorder}`); - this.inputRoot.setAttribute('height', `${heightMinusBorder}`); + this.editor.updateSize( + new Size(widthMinusBorder, heightMinusBorder), + new Size(0, 0), + ); + this.editor.getDom().setAttribute('x', `${Bubble.DOUBLE_BORDER / 2}`); + this.editor.getDom().setAttribute('y', `${Bubble.DOUBLE_BORDER / 2}`); this.resizeGroup.setAttribute('y', `${heightMinusBorder}`); if (this.workspace.RTL) { @@ -312,14 +257,6 @@ export class TextInputBubble extends Bubble { this.onSizeChange(); } - /** Handles a text change event for the text area. Calls event listeners. */ - private onTextChange() { - this.text = this.textArea.value; - for (const listener of this.textChangeListeners) { - listener(); - } - } - /** Handles a size change event for the text area. Calls event listeners. */ private onSizeChange() { for (const listener of this.sizeChangeListeners) { @@ -333,6 +270,15 @@ export class TextInputBubble extends Bubble { listener(); } } + + /** + * Returns the text editor component of this bubble. + * + * @internal + */ + getEditor() { + return this.editor; + } } Css.register(` diff --git a/core/comments/comment_editor.ts b/core/comments/comment_editor.ts index 69dadd884..ac1559c4b 100644 --- a/core/comments/comment_editor.ts +++ b/core/comments/comment_editor.ts @@ -53,6 +53,7 @@ export class CommentEditor implements IFocusableNode { 'textarea', ) as HTMLTextAreaElement; this.textArea.setAttribute('tabindex', '-1'); + this.textArea.setAttribute('dir', this.workspace.RTL ? 'RTL' : 'LTR'); dom.addClass(this.textArea, 'blocklyCommentText'); dom.addClass(this.textArea, 'blocklyTextarea'); dom.addClass(this.textArea, 'blocklyText'); @@ -86,6 +87,11 @@ export class CommentEditor implements IFocusableNode { }, ); + // Don't zoom with mousewheel; let it scroll instead. + browserEvents.conditionalBind(this.textArea, 'wheel', this, (e: Event) => { + e.stopPropagation(); + }); + // Register listener for keydown events that would finish editing. browserEvents.conditionalBind( this.textArea, diff --git a/core/comments/rendered_workspace_comment.ts b/core/comments/rendered_workspace_comment.ts index 3457e611a..c4c1f3d4e 100644 --- a/core/comments/rendered_workspace_comment.ts +++ b/core/comments/rendered_workspace_comment.ts @@ -74,15 +74,6 @@ export class RenderedWorkspaceComment this, this.startGesture, ); - // Don't zoom with mousewheel; let it scroll instead. - browserEvents.conditionalBind( - this.view.getSvgRoot(), - 'wheel', - this, - (e: Event) => { - e.stopPropagation(); - }, - ); } /** diff --git a/core/icons/comment_icon.ts b/core/icons/comment_icon.ts index 959eb2500..8f5a82c0d 100644 --- a/core/icons/comment_icon.ts +++ b/core/icons/comment_icon.ts @@ -11,7 +11,6 @@ import type {BlockSvg} from '../block_svg.js'; import {TextInputBubble} from '../bubbles/textinput_bubble.js'; import {EventType} from '../events/type.js'; import * as eventUtils from '../events/utils.js'; -import type {IBubble} from '../interfaces/i_bubble.js'; import type {IHasBubble} from '../interfaces/i_has_bubble.js'; import type {ISerializable} from '../interfaces/i_serializable.js'; import * as renderManagement from '../render_management.js'; @@ -62,7 +61,7 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable { /** * The visibility of the bubble for this comment. * - * This is used to track what the visibile state /should/ be, not necessarily + * This is used to track what the visible state /should/ be, not necessarily * what it currently /is/. E.g. sometimes this will be true, but the block * hasn't been rendered yet, so the bubble will not currently be visible. */ @@ -340,7 +339,7 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable { } /** See IHasBubble.getBubble. */ - getBubble(): IBubble | null { + getBubble(): TextInputBubble | null { return this.textInputBubble; } @@ -365,6 +364,7 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable { this.sourceBlock.workspace as WorkspaceSvg, this.getAnchorLocation(), this.getBubbleOwnerRect(), + this, ); this.textInputBubble.setText(this.getText()); this.textInputBubble.setSize(this.bubbleSize, true); diff --git a/core/keyboard_nav/block_comment_navigation_policy.ts b/core/keyboard_nav/block_comment_navigation_policy.ts new file mode 100644 index 000000000..f2f1ab7e1 --- /dev/null +++ b/core/keyboard_nav/block_comment_navigation_policy.ts @@ -0,0 +1,76 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {TextInputBubble} from '../bubbles/textinput_bubble.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; +import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; + +/** + * Set of rules controlling keyboard navigation from an TextInputBubble. + */ +export class BlockCommentNavigationPolicy + implements INavigationPolicy +{ + /** + * Returns the first child of the given block comment. + * + * @param current The block comment to return the first child of. + * @returns The text editor of the given block comment bubble. + */ + getFirstChild(current: TextInputBubble): IFocusableNode | null { + return current.getEditor(); + } + + /** + * Returns the parent of the given block comment. + * + * @param current The block comment to return the parent of. + * @returns The parent block of the given block comment. + */ + getParent(current: TextInputBubble): IFocusableNode | null { + return current.getOwner() ?? null; + } + + /** + * Returns the next peer node of the given block comment. + * + * @param _current The block comment to find the following element of. + * @returns Null. + */ + getNextSibling(_current: TextInputBubble): IFocusableNode | null { + return null; + } + + /** + * Returns the previous peer node of the given block comment. + * + * @param _current The block comment to find the preceding element of. + * @returns Null. + */ + getPreviousSibling(_current: TextInputBubble): IFocusableNode | null { + return null; + } + + /** + * Returns whether or not the given block comment can be navigated to. + * + * @param current The instance to check for navigability. + * @returns True if the given block comment can be focused. + */ + isNavigable(current: TextInputBubble): boolean { + return current.canBeFocused(); + } + + /** + * Returns whether the given object can be navigated from by this policy. + * + * @param current The object to check if this policy applies to. + * @returns True if the object is an TextInputBubble. + */ + isApplicable(current: any): current is TextInputBubble { + return current instanceof TextInputBubble; + } +} diff --git a/core/keyboard_nav/comment_editor_navigation_policy.ts b/core/keyboard_nav/comment_editor_navigation_policy.ts new file mode 100644 index 000000000..456df8e97 --- /dev/null +++ b/core/keyboard_nav/comment_editor_navigation_policy.ts @@ -0,0 +1,54 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {CommentEditor} from '../comments/comment_editor.js'; +import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; +import type {INavigationPolicy} from '../interfaces/i_navigation_policy.js'; + +/** + * Set of rules controlling keyboard navigation from a comment editor. + * This is a no-op placeholder (other than isNavigable/isApplicable) since + * comment editors handle their own navigation when editing ends. + */ +export class CommentEditorNavigationPolicy + implements INavigationPolicy +{ + getFirstChild(_current: CommentEditor): IFocusableNode | null { + return null; + } + + getParent(_current: CommentEditor): IFocusableNode | null { + return null; + } + + getNextSibling(_current: CommentEditor): IFocusableNode | null { + return null; + } + + getPreviousSibling(_current: CommentEditor): IFocusableNode | null { + return null; + } + + /** + * Returns whether or not the given comment editor can be navigated to. + * + * @param current The instance to check for navigability. + * @returns False. + */ + isNavigable(current: CommentEditor): boolean { + return current.canBeFocused(); + } + + /** + * Returns whether the given object can be navigated from by this policy. + * + * @param current The object to check if this policy applies to. + * @returns True if the object is a CommentEditor. + */ + isApplicable(current: any): current is CommentEditor { + return current instanceof CommentEditor; + } +} diff --git a/core/keyboard_nav/icon_navigation_policy.ts b/core/keyboard_nav/icon_navigation_policy.ts index 70631ce81..112239d06 100644 --- a/core/keyboard_nav/icon_navigation_policy.ts +++ b/core/keyboard_nav/icon_navigation_policy.ts @@ -6,6 +6,7 @@ import {BlockSvg} from '../block_svg.js'; import {getFocusManager} from '../focus_manager.js'; +import {CommentIcon} from '../icons/comment_icon.js'; import {Icon} from '../icons/icon.js'; import {MutatorIcon} from '../icons/mutator_icon.js'; import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; @@ -29,6 +30,12 @@ export class IconNavigationPolicy implements INavigationPolicy { getFocusManager().getFocusedNode() === current ) { return current.getBubble()?.getWorkspace() ?? null; + } else if ( + current instanceof CommentIcon && + current.bubbleIsVisible() && + getFocusManager().getFocusedNode() === current + ) { + return current.getBubble()?.getEditor() ?? null; } return null; diff --git a/core/navigator.ts b/core/navigator.ts index 2f095f6f9..9c7c22f59 100644 --- a/core/navigator.ts +++ b/core/navigator.ts @@ -6,8 +6,10 @@ import type {IFocusableNode} from './interfaces/i_focusable_node.js'; import type {INavigationPolicy} from './interfaces/i_navigation_policy.js'; +import {BlockCommentNavigationPolicy} from './keyboard_nav/block_comment_navigation_policy.js'; import {BlockNavigationPolicy} from './keyboard_nav/block_navigation_policy.js'; import {CommentBarButtonNavigationPolicy} from './keyboard_nav/comment_bar_button_navigation_policy.js'; +import {CommentEditorNavigationPolicy} from './keyboard_nav/comment_editor_navigation_policy.js'; import {ConnectionNavigationPolicy} from './keyboard_nav/connection_navigation_policy.js'; import {FieldNavigationPolicy} from './keyboard_nav/field_navigation_policy.js'; import {IconNavigationPolicy} from './keyboard_nav/icon_navigation_policy.js'; @@ -33,6 +35,8 @@ export class Navigator { new IconNavigationPolicy(), new WorkspaceCommentNavigationPolicy(), new CommentBarButtonNavigationPolicy(), + new BlockCommentNavigationPolicy(), + new CommentEditorNavigationPolicy(), ]; /** diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index d713f11cf..4180c1099 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -22,6 +22,7 @@ import type {Block} from './block.js'; import type {BlockSvg} from './block_svg.js'; import type {BlocklyOptions} from './blockly_options.js'; import * as browserEvents from './browser_events.js'; +import {TextInputBubble} from './bubbles/textinput_bubble.js'; import {COMMENT_COLLAPSE_BAR_BUTTON_FOCUS_IDENTIFIER} from './comments/collapse_comment_bar_button.js'; import {COMMENT_EDITOR_FOCUS_IDENTIFIER} from './comments/comment_editor.js'; import {COMMENT_DELETE_BAR_BUTTON_FOCUS_IDENTIFIER} from './comments/delete_comment_bar_button.js'; @@ -2868,6 +2869,11 @@ export class WorkspaceSvg bubble.getFocusableElement().id === id ) { return bubble; + } else if ( + bubble instanceof TextInputBubble && + bubble.getEditor().getFocusableElement().id === id + ) { + return bubble.getEditor(); } } } From 2c6c2e1a9d3addd72c5c5cc697f7bd246d6cee1e Mon Sep 17 00:00:00 2001 From: Erik Pasternak Date: Fri, 11 Jul 2025 13:50:22 -0700 Subject: [PATCH 02/38] fix: Fix toolbox categories tests (Almost) This fixes the the toolbox categories tests except for dragging out the four empty statement blocks, which is fixed by https://github.com/google/blockly-samples/pull/2580 Once the latest version of samples is published the toolbox suite can be re-enabled. --- tests/browser/test/toolbox_drag_test.mjs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/browser/test/toolbox_drag_test.mjs b/tests/browser/test/toolbox_drag_test.mjs index 32c201406..d2a7ecce1 100644 --- a/tests/browser/test/toolbox_drag_test.mjs +++ b/tests/browser/test/toolbox_drag_test.mjs @@ -11,7 +11,7 @@ import * as chai from 'chai'; import {Key} from 'webdriverio'; import { - dragBlockTypeFromFlyout, + getBlockTypeFromCategory, getCategory, PAUSE_TIME, screenDirection, @@ -148,7 +148,12 @@ async function openCategories(browser, categoryList, directionMultiplier) { continue; } const blockType = await getNthBlockType(browser, categoryName, i); - dragBlockTypeFromFlyout(browser, categoryName, blockType, 50, 20); + const blockElem = await getBlockTypeFromCategory( + browser, + categoryName, + blockType, + ); + await blockElem.dragAndDrop({x: 50 * directionMultiplier, y: 20}); await browser.pause(PAUSE_TIME); // Should be one top level block on the workspace. const topBlockCount = await browser.execute(() => { From 802d3f887dd9e7559f9116b4ba6e42dd805dd168 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 08:35:50 -0700 Subject: [PATCH 03/38] chore(deps): bump google-closure-compiler (#9233) Bumps [google-closure-compiler](https://github.com/google/closure-compiler-npm) from 20250625.0.0 to 20250709.0.0. - [Release notes](https://github.com/google/closure-compiler-npm/releases) - [Commits](https://github.com/google/closure-compiler-npm/compare/v20250625.0.0...v20250709.0.0) --- updated-dependencies: - dependency-name: google-closure-compiler dependency-version: 20250709.0.0 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 56 ++++++++++++++++++++++++++--------------------- package.json | 2 +- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index 21178897d..6a60c9a36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,7 @@ "eslint-plugin-prettier": "^5.2.1", "glob": "^11.0.1", "globals": "^16.0.0", - "google-closure-compiler": "^20250625.0.0", + "google-closure-compiler": "^20250709.0.0", "gulp": "^5.0.0", "gulp-concat": "^2.6.1", "gulp-gzip": "^1.4.2", @@ -4949,13 +4949,14 @@ } }, "node_modules/google-closure-compiler": { - "version": "20250625.0.0", - "resolved": "https://registry.npmjs.org/google-closure-compiler/-/google-closure-compiler-20250625.0.0.tgz", - "integrity": "sha512-FQ6yKCRYwo4493Rq6lZrxpmWuJGZuuSruCdtArptkoThadzw4TM0YvQJvwRYnQDUpjj6/x7G14l2n/+8G39AIA==", + "version": "20250709.0.0", + "resolved": "https://registry.npmjs.org/google-closure-compiler/-/google-closure-compiler-20250709.0.0.tgz", + "integrity": "sha512-FUdjG7vri7Pi/iswJj1bFcE3cYOcGLnez2nKaEK8qSailRFQlnp8j9vuT60EOU8FLzckEPI0Sf882Q7vJPilFg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "chalk": "5.x", - "google-closure-compiler-java": "^20250625.0.0", + "google-closure-compiler-java": "^20250709.0.0", "minimist": "1.x", "vinyl": "3.x", "vinyl-sourcemaps-apply": "^0.2.0" @@ -4967,67 +4968,72 @@ "node": ">=18" }, "optionalDependencies": { - "google-closure-compiler-linux": "^20250625.0.0", - "google-closure-compiler-linux-arm64": "^20250625.0.0", - "google-closure-compiler-macos": "^20250625.0.0", - "google-closure-compiler-windows": "^20250625.0.0" + "google-closure-compiler-linux": "^20250709.0.0", + "google-closure-compiler-linux-arm64": "^20250709.0.0", + "google-closure-compiler-macos": "^20250709.0.0", + "google-closure-compiler-windows": "^20250709.0.0" } }, "node_modules/google-closure-compiler-java": { - "version": "20250625.0.0", - "resolved": "https://registry.npmjs.org/google-closure-compiler-java/-/google-closure-compiler-java-20250625.0.0.tgz", - "integrity": "sha512-T916Kvb7JYaIiH9spiJXVKeualLV7PO/KXOJzMhLrW4M6etfvr3s2cTqlhUk+BrxvgxqWBWFbMDRUZbVGPnBaw==", - "dev": true + "version": "20250709.0.0", + "resolved": "https://registry.npmjs.org/google-closure-compiler-java/-/google-closure-compiler-java-20250709.0.0.tgz", + "integrity": "sha512-gyriPJ8nYxYVa5wqeMJZsOdFoDDcHSmGHG9VNYjQrcdIOWyxW9Ggcb2gtrI/MEa54CLoRbzUJ12ELO1mzePMlQ==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/google-closure-compiler-linux": { - "version": "20250625.0.0", - "resolved": "https://registry.npmjs.org/google-closure-compiler-linux/-/google-closure-compiler-linux-20250625.0.0.tgz", - "integrity": "sha512-2cOYLfG7RF49FnGG+yBGlEndE0es8D7+YIGgF8KnGIkxrfiZhOTyQftFx4z48TZ1Be/1JtM2eNXbD2fuR9nJdA==", + "version": "20250709.0.0", + "resolved": "https://registry.npmjs.org/google-closure-compiler-linux/-/google-closure-compiler-linux-20250709.0.0.tgz", + "integrity": "sha512-kpl9W+696vnGzpa/ewfwpsRR3t42g3CDQ5hFjQAitxtZpnejU7ik94+O8D+56049zS2O85LdWRDCbckvzEXw+w==", "cpu": [ "x32", "x64" ], "dev": true, + "license": "Apache-2.0", "optional": true, "os": [ "linux" ] }, "node_modules/google-closure-compiler-linux-arm64": { - "version": "20250625.0.0", - "resolved": "https://registry.npmjs.org/google-closure-compiler-linux-arm64/-/google-closure-compiler-linux-arm64-20250625.0.0.tgz", - "integrity": "sha512-2vKY8UpL03CFe+k1qFma/HnUZnTM3V3K5ukxmk/Xwt3D7CTwn/039zA3AjxsGW5vLp4guVyLtqbS711KeGpLNA==", + "version": "20250709.0.0", + "resolved": "https://registry.npmjs.org/google-closure-compiler-linux-arm64/-/google-closure-compiler-linux-arm64-20250709.0.0.tgz", + "integrity": "sha512-3mLAD9JpAM0StUb2VTOw4L/rIxksTO7lOfuI0+OyexQfLIRLM8M9jeUgrOAPbmgDsyYZ8Q3pHX2qcnURexZsrw==", "cpu": [ "arm64" ], "dev": true, + "license": "Apache-2.0", "optional": true, "os": [ "linux" ] }, "node_modules/google-closure-compiler-macos": { - "version": "20250625.0.0", - "resolved": "https://registry.npmjs.org/google-closure-compiler-macos/-/google-closure-compiler-macos-20250625.0.0.tgz", - "integrity": "sha512-/S3d5/oKKw2pEu42Bn+fnoKR0cAjlhOQP1IM0D1aDqNS+jMUXo4bV7RSVB+NSVL65XxIVQOqbnkD5Cfoe8lbrw==", + "version": "20250709.0.0", + "resolved": "https://registry.npmjs.org/google-closure-compiler-macos/-/google-closure-compiler-macos-20250709.0.0.tgz", + "integrity": "sha512-2/MXSVgM+HmnzwbyWdfY2ZVjKgK8LFtCKhsQQhsSV/f2jnrHcuG9+RkzLrzQsO1zPpHaLcXAkizf4AUpCfuzBA==", "cpu": [ "arm64" ], "dev": true, + "license": "Apache-2.0", "optional": true, "os": [ "darwin" ] }, "node_modules/google-closure-compiler-windows": { - "version": "20250625.0.0", - "resolved": "https://registry.npmjs.org/google-closure-compiler-windows/-/google-closure-compiler-windows-20250625.0.0.tgz", - "integrity": "sha512-YBNRFTSuWXDJad1pJ1SPjPFpgImrQr7XeW1D9YrPCv1T5cfM8vy01jFkZIDuUha38kHsPvk7kG3rkYYrJpD8+Q==", + "version": "20250709.0.0", + "resolved": "https://registry.npmjs.org/google-closure-compiler-windows/-/google-closure-compiler-windows-20250709.0.0.tgz", + "integrity": "sha512-ZnmgRzx0qIVQu0zw7ZTJQz3tMFVhwzeODZfXRnYDLeNkJA7IBaWsNHTALA7pUcgPM+YDDr4ihQOexMc0u4s7LQ==", "cpu": [ "x32", "x64" ], "dev": true, + "license": "Apache-2.0", "optional": true, "os": [ "win32" diff --git a/package.json b/package.json index d464ae5f1..4df4f9586 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,7 @@ "eslint-plugin-prettier": "^5.2.1", "glob": "^11.0.1", "globals": "^16.0.0", - "google-closure-compiler": "^20250625.0.0", + "google-closure-compiler": "^20250709.0.0", "gulp": "^5.0.0", "gulp-concat": "^2.6.1", "gulp-gzip": "^1.4.2", From 52634e4dec56de31c790d3e718cebffef147f27e Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 14 Jul 2025 08:55:13 -0700 Subject: [PATCH 04/38] fix: Focus the first element in flyouts. (#9228) * fix: Focus the first element in flyouts. * refactor: Adjust retrieval of flyout elements. --- core/workspace_svg.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 4180c1099..b666dc97a 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -2727,6 +2727,19 @@ export class WorkspaceSvg previousNode: IFocusableNode | null, ): IFocusableNode | null { if (!previousNode) { + const flyout = this.targetWorkspace?.getFlyout(); + if (this.isFlyout && flyout) { + // Return the first focusable item of the flyout. + return ( + flyout + .getContents() + .find((flyoutItem) => { + const element = flyoutItem.getElement(); + return isFocusableNode(element) && element.canBeFocused(); + }) + ?.getElement() ?? null + ); + } return this.getTopBlocks(true)[0] ?? null; } else return null; } From 9f66f0c59608d82547e06d5f6ec1b5228116b111 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 14 Jul 2025 09:53:17 -0700 Subject: [PATCH 05/38] fix: Fire a `VarTypeChange` event when changing a variable's type. (#9236) --- core/variable_map.ts | 10 ++++++++++ tests/mocha/variable_map_test.js | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/core/variable_map.ts b/core/variable_map.ts index 403893332..3dd4bf547 100644 --- a/core/variable_map.ts +++ b/core/variable_map.ts @@ -109,6 +109,9 @@ export class VariableMap variable: IVariableModel, newType: string, ): IVariableModel { + const oldType = variable.getType(); + if (oldType === newType) return variable; + this.variableMap.get(variable.getType())?.delete(variable.getId()); variable.setType(newType); const newTypeVariables = @@ -118,6 +121,13 @@ export class VariableMap if (!this.variableMap.has(newType)) { this.variableMap.set(newType, newTypeVariables); } + eventUtils.fire( + new (eventUtils.get(EventType.VAR_TYPE_CHANGE))( + variable, + oldType, + newType, + ), + ); return variable; } diff --git a/tests/mocha/variable_map_test.js b/tests/mocha/variable_map_test.js index c02887cea..2d6cee0b9 100644 --- a/tests/mocha/variable_map_test.js +++ b/tests/mocha/variable_map_test.js @@ -505,5 +505,26 @@ suite('Variable Map', function () { }); }); }); + + suite('variable type change events', function () { + test('are fired when a variable has its type changed', function () { + const variable = this.variableMap.createVariable( + 'name1', + 'type1', + 'id1', + ); + this.variableMap.changeVariableType(variable, 'type2'); + assertEventFired( + this.eventSpy, + Blockly.Events.VarTypeChange, + { + oldType: 'type1', + newType: 'type2', + varId: 'id1', + }, + this.workspace.id, + ); + }); + }); }); }); From 55bd1aff795e59ef56748735b3f45cb882be2433 Mon Sep 17 00:00:00 2001 From: Erik Pasternak Date: Mon, 14 Jul 2025 10:09:51 -0700 Subject: [PATCH 06/38] Enable categories test --- tests/browser/test/toolbox_drag_test.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/browser/test/toolbox_drag_test.mjs b/tests/browser/test/toolbox_drag_test.mjs index d2a7ecce1..66b74d9e2 100644 --- a/tests/browser/test/toolbox_drag_test.mjs +++ b/tests/browser/test/toolbox_drag_test.mjs @@ -179,9 +179,9 @@ async function openCategories(browser, categoryList, directionMultiplier) { chai.assert.equal(failureCount, 0); } -// TODO (#9217) These take too long to run and are very flakey. Need to find a -// better way to test whatever this is trying to test. -suite.skip('Open toolbox categories', function () { +// TODO (#9217) These take too long to run and are very flakey. Need to pull +// these out into their own test runner. +suite('Open toolbox categories', function () { this.timeout(0); test('opening every toolbox category in the category toolbox in LTR', async function () { From ec01df4adaa407301b4eac5d4cd9c0526c677a5c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 18:11:03 +0100 Subject: [PATCH 07/38] chore(deps): bump @microsoft/api-documenter from 7.26.26 to 7.26.29 (#9234) Bumps [@microsoft/api-documenter](https://github.com/microsoft/rushstack/tree/HEAD/apps/api-documenter) from 7.26.26 to 7.26.29. - [Changelog](https://github.com/microsoft/rushstack/blob/main/apps/api-documenter/CHANGELOG.md) - [Commits](https://github.com/microsoft/rushstack/commits/@microsoft/api-documenter_v7.26.29/apps/api-documenter) --- updated-dependencies: - dependency-name: "@microsoft/api-documenter" dependency-version: 7.26.29 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6a60c9a36..1bdb22c75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -912,9 +912,9 @@ } }, "node_modules/@microsoft/api-documenter": { - "version": "7.26.26", - "resolved": "https://registry.npmjs.org/@microsoft/api-documenter/-/api-documenter-7.26.26.tgz", - "integrity": "sha512-085FwdwQcXGvwtMJFajwhu5eZOQ3PXsyLIoq3WXAQr/7M6Vn59GMGjuB/+lIXqmWKkxzeFAX5f9sKqr9X7zI3g==", + "version": "7.26.29", + "resolved": "https://registry.npmjs.org/@microsoft/api-documenter/-/api-documenter-7.26.29.tgz", + "integrity": "sha512-5gqnUCut1BeNmOZIE8hUJbzq3DxFcAyXL12oF6aFVtTDF8WiVs/J1HtlLYbxeIff6qbI1LfLnr16t+WOm9UVJw==", "dev": true, "license": "MIT", "dependencies": { From 7479c2b5c74b984dbf3d8ed638a7d4d03560d1c2 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 14 Jul 2025 10:22:09 -0700 Subject: [PATCH 08/38] fix: Fix order of arguments in `IVariableMap.createVariable()`. (#9231) --- core/interfaces/i_variable_map.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/interfaces/i_variable_map.ts b/core/interfaces/i_variable_map.ts index 6c21aa8e0..22b4eda90 100644 --- a/core/interfaces/i_variable_map.ts +++ b/core/interfaces/i_variable_map.ts @@ -43,7 +43,7 @@ export interface IVariableMap> { * Creates a new variable with the given name. If ID is not specified, the * variable map should create one. Returns the new variable. */ - createVariable(name: string, id?: string, type?: string | null): T; + createVariable(name: string, type?: string, id?: string | null): T; /* Adds a variable to this variable map. */ addVariable(variable: T): void; From c1c7ddb2938183b9b0a62fe4f618b3976c9ff414 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 11:17:27 -0700 Subject: [PATCH 09/38] chore(deps): bump chai from 5.2.0 to 5.2.1 (#9235) Bumps [chai](https://github.com/chaijs/chai) from 5.2.0 to 5.2.1. - [Release notes](https://github.com/chaijs/chai/releases) - [Changelog](https://github.com/chaijs/chai/blob/main/History.md) - [Commits](https://github.com/chaijs/chai/compare/v5.2.0...v5.2.1) --- updated-dependencies: - dependency-name: chai dependency-version: 5.2.1 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1bdb22c75..0db79293f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2766,9 +2766,9 @@ } }, "node_modules/chai": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", - "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.1.tgz", + "integrity": "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==", "dev": true, "license": "MIT", "dependencies": { @@ -2779,7 +2779,7 @@ "pathval": "^2.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/chalk": { From 908712e19d2d028df9352f55be9046a47eeae303 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 15 Jul 2025 11:06:27 -0700 Subject: [PATCH 10/38] fix: Refer to correct replacement method in deprecation warning. (#9237) --- core/workspace.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/workspace.ts b/core/workspace.ts index 5f2051939..88745f420 100644 --- a/core/workspace.ts +++ b/core/workspace.ts @@ -469,7 +469,7 @@ export class Workspace { 'Blockly.Workspace.getVariableUsesById', 'v12', 'v13', - 'Blockly.Workspace.getVariableMap().getVariableUsesById', + 'Blockly.Variables.getVariableUsesById', ); return getVariableUsesById(this, id); } From 3c7add57eed7e2564a69f650d8381db3d1f2eadd Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Fri, 18 Jul 2025 14:27:49 -0700 Subject: [PATCH 11/38] fix: Make non-autoclosing flyouts stay open. (#9245) * chore: Add tests for toolbox/flyout/focus autoclose behavior. * fix: Don't force-close non-autoclosing flyouts. --- core/toolbox/toolbox.ts | 10 ++------ core/workspace_svg.ts | 4 +-- tests/mocha/toolbox_test.js | 51 +++++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/core/toolbox/toolbox.ts b/core/toolbox/toolbox.ts index 4979fdfa4..f34034d33 100644 --- a/core/toolbox/toolbox.ts +++ b/core/toolbox/toolbox.ts @@ -22,10 +22,7 @@ import '../events/events_toolbox_item_select.js'; import {EventType} from '../events/type.js'; import * as eventUtils from '../events/utils.js'; import {getFocusManager} from '../focus_manager.js'; -import { - isAutoHideable, - type IAutoHideable, -} from '../interfaces/i_autohideable.js'; +import {type IAutoHideable} from '../interfaces/i_autohideable.js'; import type {ICollapsibleToolboxItem} from '../interfaces/i_collapsible_toolbox_item.js'; import {isDeletable} from '../interfaces/i_deletable.js'; import type {IDraggable} from '../interfaces/i_draggable.js'; @@ -1150,10 +1147,7 @@ export class Toolbox // If navigating to anything other than the toolbox's flyout then clear the // selection so that the toolbox's flyout can automatically close. if (!nextTree || nextTree !== this.flyout?.getWorkspace()) { - this.clearSelection(); - if (this.flyout && isAutoHideable(this.flyout)) { - this.flyout.autoHide(false); - } + this.autoHide(false); } } } diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index b666dc97a..6c6b59301 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -2908,11 +2908,9 @@ export class WorkspaceSvg // Only hide the flyout if the flyout's workspace is losing focus and that // focus isn't returning to the flyout itself, the toolbox, or ephemeral. if (getFocusManager().ephemeralFocusTaken()) return; - const flyout = this.targetWorkspace.getFlyout(); const toolbox = this.targetWorkspace.getToolbox(); if (toolbox && nextTree === toolbox) return; - if (toolbox) toolbox.clearSelection(); - if (flyout && isAutoHideable(flyout)) flyout.autoHide(false); + if (isAutoHideable(toolbox)) toolbox.autoHide(false); } } diff --git a/tests/mocha/toolbox_test.js b/tests/mocha/toolbox_test.js index f32319c67..4e92cd28f 100644 --- a/tests/mocha/toolbox_test.js +++ b/tests/mocha/toolbox_test.js @@ -183,6 +183,57 @@ suite('Toolbox', function () { }); }); + suite('focus management', function () { + setup(function () { + this.toolbox = getInjectedToolbox(); + }); + teardown(function () { + this.toolbox.dispose(); + }); + + test('Losing focus hides autoclosing flyout', function () { + // Focus the toolbox and select a category to open the flyout. + const target = this.toolbox.HtmlDiv.querySelector( + '.blocklyToolboxCategory', + ); + Blockly.getFocusManager().focusNode(this.toolbox); + target.dispatchEvent( + new PointerEvent('pointerdown', { + target, + bubbles: true, + }), + ); + assert.isTrue(this.toolbox.getFlyout().isVisible()); + + // Focus the workspace to trigger the toolbox to close the flyout. + Blockly.getFocusManager().focusNode(this.toolbox.getWorkspace()); + assert.isFalse(this.toolbox.getFlyout().isVisible()); + }); + + test('Losing focus does not hide non-autoclosing flyout', function () { + // Make the toolbox's flyout non-autoclosing. + this.toolbox.getFlyout().setAutoClose(false); + + // Focus the toolbox and select a category to open the flyout. + const target = this.toolbox.HtmlDiv.querySelector( + '.blocklyToolboxCategory', + ); + Blockly.getFocusManager().focusNode(this.toolbox); + target.dispatchEvent( + new PointerEvent('pointerdown', { + target, + bubbles: true, + }), + ); + assert.isTrue(this.toolbox.getFlyout().isVisible()); + + // Focus the workspace; this should *not* trigger the toolbox to close the + // flyout, which should remain visible. + Blockly.getFocusManager().focusNode(this.toolbox.getWorkspace()); + assert.isTrue(this.toolbox.getFlyout().isVisible()); + }); + }); + suite('onClick_', function () { setup(function () { this.toolbox = getInjectedToolbox(); From f37e7fede20ed81237f65626cdb1dd20c8f6d5e3 Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Thu, 24 Jul 2025 12:28:18 -0400 Subject: [PATCH 12/38] chore: fix docs generation script (#9251) * chore: pin api-documenter to a version that uses markdown tables * chore: fix docs generation errors * chore: update patch for api-documenter --- package-lock.json | 346 +++++++++++++++--- package.json | 3 +- ...=> @microsoft+api-documenter+7.22.4.patch} | 25 +- scripts/gulpfiles/docs_tasks.mjs | 4 +- 4 files changed, 306 insertions(+), 72 deletions(-) rename patches/{@microsoft+api-documenter+7.26.26.patch => @microsoft+api-documenter+7.22.4.patch} (85%) diff --git a/package-lock.json b/package-lock.json index 0db79293f..28f32b019 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,8 +18,9 @@ "@blockly/theme-modern": "^6.0.3", "@hyperjump/browser": "^1.1.4", "@hyperjump/json-schema": "^1.5.0", - "@microsoft/api-documenter": "^7.22.4", + "@microsoft/api-documenter": "7.22.4", "@microsoft/api-extractor": "^7.29.5", + "ajv": "^8.17.1", "async-done": "^2.0.0", "chai": "^5.1.1", "concurrently": "^9.0.1", @@ -485,6 +486,23 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -498,6 +516,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/@eslint/js": { "version": "9.30.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.0.tgz", @@ -912,17 +937,17 @@ } }, "node_modules/@microsoft/api-documenter": { - "version": "7.26.29", - "resolved": "https://registry.npmjs.org/@microsoft/api-documenter/-/api-documenter-7.26.29.tgz", - "integrity": "sha512-5gqnUCut1BeNmOZIE8hUJbzq3DxFcAyXL12oF6aFVtTDF8WiVs/J1HtlLYbxeIff6qbI1LfLnr16t+WOm9UVJw==", + "version": "7.22.4", + "resolved": "https://registry.npmjs.org/@microsoft/api-documenter/-/api-documenter-7.22.4.tgz", + "integrity": "sha512-d4htEhBd8UkFKff/+/nAi/z7rrspm1DanFmsRHLUp4gKMo/8hYDH/IQBWB4r9X/8X72jCv3I++VVWAfichL1rw==", "dev": true, "license": "MIT", "dependencies": { - "@microsoft/api-extractor-model": "7.30.6", - "@microsoft/tsdoc": "~0.15.1", - "@rushstack/node-core-library": "5.13.1", - "@rushstack/terminal": "0.15.3", - "@rushstack/ts-command-line": "5.0.1", + "@microsoft/api-extractor-model": "7.26.8", + "@microsoft/tsdoc": "0.14.2", + "@rushstack/node-core-library": "3.58.0", + "@rushstack/ts-command-line": "4.13.2", + "colors": "~1.2.1", "js-yaml": "~3.13.1", "resolve": "~1.22.1" }, @@ -930,6 +955,106 @@ "api-documenter": "bin/api-documenter" } }, + "node_modules/@microsoft/api-documenter/node_modules/@microsoft/api-extractor-model": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.26.8.tgz", + "integrity": "sha512-ESj3bBJkiMg/8tS0PW4+2rUgTVwOEfy41idTnFgdbVX+O50bN6S99MV6FIPlCZWCnRDcBfwxRXLdAkOQQ0JqGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "~0.16.1", + "@rushstack/node-core-library": "3.58.0" + } + }, + "node_modules/@microsoft/api-documenter/node_modules/@microsoft/tsdoc": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz", + "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@microsoft/api-documenter/node_modules/@microsoft/tsdoc-config": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.16.2.tgz", + "integrity": "sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.14.2", + "ajv": "~6.12.6", + "jju": "~1.4.0", + "resolve": "~1.19.0" + } + }, + "node_modules/@microsoft/api-documenter/node_modules/@microsoft/tsdoc-config/node_modules/resolve": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@microsoft/api-documenter/node_modules/@rushstack/node-core-library": { + "version": "3.58.0", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.58.0.tgz", + "integrity": "sha512-DHAZ3LTOEq2/EGURznpTJDnB3SNE2CKMDXuviQ6afhru6RykE3QoqXkeyjbpLb5ib5cpIRCPE/wykNe0xmQj3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "colors": "~1.2.1", + "fs-extra": "~7.0.1", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.22.1", + "semver": "~7.3.0", + "z-schema": "~5.0.2" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@microsoft/api-documenter/node_modules/@rushstack/ts-command-line": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.13.2.tgz", + "integrity": "sha512-bCU8qoL9HyWiciltfzg7GqdfODUeda/JpI0602kbN5YH22rzTxyqYvv7aRLENCM7XCQ1VRs7nMkEqgJUOU8Sag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/argparse": "1.0.38", + "argparse": "~1.0.9", + "colors": "~1.2.1", + "string-argv": "~0.3.1" + } + }, + "node_modules/@microsoft/api-documenter/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/@microsoft/api-documenter/node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -952,6 +1077,29 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/@microsoft/api-documenter/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@microsoft/api-documenter/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@microsoft/api-extractor": { "version": "7.52.8", "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.52.8.tgz", @@ -1056,12 +1204,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@microsoft/tsdoc-config/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1236,13 +1378,6 @@ "node": ">=14.14" } }, - "node_modules/@rushstack/node-core-library/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, "node_modules/@rushstack/rig-package": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.5.3.tgz", @@ -1955,16 +2090,16 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -2002,28 +2137,6 @@ } } }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.14.0.tgz", - "integrity": "sha512-oYs1UUtO97ZO2lJ4bwnWeQW8/zvOIQLGKcvPTsWmvc2SYgBb+upuNS5NxoLaMU4h8Ju3Nbj6Cq8mD2LQoqVKFA==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.4.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, "node_modules/ansi-gray": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", @@ -2979,6 +3092,16 @@ "color-support": "bin.js" } }, + "node_modules/colors": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.2.5.tgz", + "integrity": "sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/commander": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", @@ -4068,6 +4191,23 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/eslint/node_modules/eslint-visitor-keys": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", @@ -4092,6 +4232,13 @@ "node": ">=10.13.0" } }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -4340,6 +4487,23 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fast-xml-parser": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", @@ -4598,6 +4762,41 @@ "node": ">=12.20.0" } }, + "node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-extra/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/fs-extra/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/fs-mkdirp-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-2.0.1.tgz", @@ -6198,9 +6397,9 @@ "dev": true }, "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, "license": "MIT" }, @@ -6486,6 +6685,14 @@ "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", "dev": true }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -9207,6 +9414,16 @@ "node": ">= 10.13.0" } }, + "node_modules/validator": { + "version": "13.15.15", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", + "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/value-or-function": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-4.0.0.tgz", @@ -9758,6 +9975,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + }, "node_modules/zip-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", diff --git a/package.json b/package.json index 4df4f9586..dfff0da7f 100644 --- a/package.json +++ b/package.json @@ -105,8 +105,9 @@ "@blockly/theme-modern": "^6.0.3", "@hyperjump/browser": "^1.1.4", "@hyperjump/json-schema": "^1.5.0", - "@microsoft/api-documenter": "^7.22.4", + "@microsoft/api-documenter": "7.22.4", "@microsoft/api-extractor": "^7.29.5", + "ajv": "^8.17.1", "async-done": "^2.0.0", "chai": "^5.1.1", "concurrently": "^9.0.1", diff --git a/patches/@microsoft+api-documenter+7.26.26.patch b/patches/@microsoft+api-documenter+7.22.4.patch similarity index 85% rename from patches/@microsoft+api-documenter+7.26.26.patch rename to patches/@microsoft+api-documenter+7.22.4.patch index de8e47c04..3cc97035d 100644 --- a/patches/@microsoft+api-documenter+7.26.26.patch +++ b/patches/@microsoft+api-documenter+7.22.4.patch @@ -1,8 +1,8 @@ diff --git a/node_modules/@microsoft/api-documenter/lib/documenters/MarkdownDocumenter.js b/node_modules/@microsoft/api-documenter/lib/documenters/MarkdownDocumenter.js -index 0f4e2ba..3af2014 100644 +index 5284d10..4f8b439 100644 --- a/node_modules/@microsoft/api-documenter/lib/documenters/MarkdownDocumenter.js +++ b/node_modules/@microsoft/api-documenter/lib/documenters/MarkdownDocumenter.js -@@ -893,12 +893,15 @@ class MarkdownDocumenter { +@@ -877,12 +877,14 @@ class MarkdownDocumenter { } _writeBreadcrumb(output, apiItem) { const configuration = this._tsdocConfiguration; @@ -19,28 +19,23 @@ index 0f4e2ba..3af2014 100644 + // linkText: 'Home', + // urlDestination: this._getLinkFilenameForApiItem(this._apiModel) + // })); -+ + let first = true; for (const hierarchyItem of apiItem.getHierarchy()) { switch (hierarchyItem.kind) { case api_extractor_model_1.ApiItemKind.Model: -@@ -908,18 +911,24 @@ class MarkdownDocumenter { +@@ -892,18 +894,23 @@ class MarkdownDocumenter { // this may change in the future. break; default: - output.appendNodesInParagraph([ -- new tsdoc_1.DocPlainText({ -- configuration, -- text: ' > ' -- }), + if (!first) { + // Only print the breadcrumb separator if it's not the first item we're printing. + output.appendNodeInParagraph( -+ new tsdoc_1.DocPlainText({ -+ configuration, -+ text: ' > ' -+ }) -+ ); + new tsdoc_1.DocPlainText({ + configuration, + text: ' > ' +- }), ++ })); + } + first = false; + output.appendNodeInParagraph( @@ -55,7 +50,7 @@ index 0f4e2ba..3af2014 100644 } } } -@@ -992,11 +1001,8 @@ class MarkdownDocumenter { +@@ -968,11 +975,8 @@ class MarkdownDocumenter { // For overloaded methods, add a suffix such as "MyClass.myMethod_2". let qualifiedName = Utilities_1.Utilities.getSafeFilenameForName(hierarchyItem.displayName); if (api_extractor_model_1.ApiParameterListMixin.isBaseClassOf(hierarchyItem)) { @@ -69,7 +64,7 @@ index 0f4e2ba..3af2014 100644 } switch (hierarchyItem.kind) { case api_extractor_model_1.ApiItemKind.Model: -@@ -1007,7 +1013,8 @@ class MarkdownDocumenter { +@@ -983,7 +987,8 @@ class MarkdownDocumenter { baseName = Utilities_1.Utilities.getSafeFilenameForName(node_core_library_1.PackageName.getUnscopedName(hierarchyItem.displayName)); break; default: diff --git a/scripts/gulpfiles/docs_tasks.mjs b/scripts/gulpfiles/docs_tasks.mjs index 63fdbe665..51abd480f 100644 --- a/scripts/gulpfiles/docs_tasks.mjs +++ b/scripts/gulpfiles/docs_tasks.mjs @@ -2,8 +2,8 @@ import {execSync} from 'child_process'; import {Extractor} from 'markdown-tables-to-json'; import * as fs from 'fs'; import * as gulp from 'gulp'; -import * as header from 'gulp-header'; -import * as replace from 'gulp-replace'; +import header from 'gulp-header'; +import replace from 'gulp-replace'; const DOCS_DIR = 'docs'; From 0de5b17c8a531f8582b83c436af186c8490c9f00 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Jul 2025 16:29:34 +0000 Subject: [PATCH 13/38] chore(deps): bump @blockly/theme-modern from 6.0.10 to 7.0.1 Bumps [@blockly/theme-modern](https://github.com/google/blockly-samples/tree/HEAD/plugins/theme-modern) from 6.0.10 to 7.0.1. - [Release notes](https://github.com/google/blockly-samples/releases) - [Changelog](https://github.com/google/blockly-samples/blob/master/plugins/theme-modern/CHANGELOG.md) - [Commits](https://github.com/google/blockly-samples/commits/@blockly/theme-modern@7.0.1/plugins/theme-modern) --- updated-dependencies: - dependency-name: "@blockly/theme-modern" dependency-version: 7.0.1 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- package-lock.json | 11 ++++++----- package.json | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 28f32b019..95e9a39b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "devDependencies": { "@blockly/block-test": "^7.0.1", "@blockly/dev-tools": "^9.0.0", - "@blockly/theme-modern": "^6.0.3", + "@blockly/theme-modern": "^7.0.1", "@hyperjump/browser": "^1.1.4", "@hyperjump/json-schema": "^1.5.0", "@microsoft/api-documenter": "7.22.4", @@ -236,15 +236,16 @@ } }, "node_modules/@blockly/theme-modern": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/@blockly/theme-modern/-/theme-modern-6.0.10.tgz", - "integrity": "sha512-xOVf5Vq5ACgbVsaNAKWb5cE0msUfBxj1G1asp0aBmWo1QCr3Yze4rUtFDaNIoeCd8EsRpuWZgBYg74zPL9eAow==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@blockly/theme-modern/-/theme-modern-7.0.1.tgz", + "integrity": "sha512-aMI3OBp8KCbLU1O14FLUlocK7IeMOyiSenlTJ4lwGcBmZntM2OIcx6o89oAIeq6HkmaH7vMlK+/AgqdB3k0y3A==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=8.17.0" }, "peerDependencies": { - "blockly": "^11.0.0" + "blockly": "^12.0.0" } }, "node_modules/@blockly/theme-tritanopia": { diff --git a/package.json b/package.json index dfff0da7f..6ae18bd4a 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "devDependencies": { "@blockly/block-test": "^7.0.1", "@blockly/dev-tools": "^9.0.0", - "@blockly/theme-modern": "^6.0.3", + "@blockly/theme-modern": "^7.0.1", "@hyperjump/browser": "^1.1.4", "@hyperjump/json-schema": "^1.5.0", "@microsoft/api-documenter": "7.22.4", From c9a8221a2d2cd01cb42aa162b7ce2b8418ec1a9c Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 28 Jul 2025 13:33:52 -0700 Subject: [PATCH 14/38] fix: Fix displaying HTML elements in `FieldDropdown`. (#9258) --- core/field_dropdown.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/core/field_dropdown.ts b/core/field_dropdown.ts index 9c0d7f292..8b01ccdda 100644 --- a/core/field_dropdown.ts +++ b/core/field_dropdown.ts @@ -699,25 +699,30 @@ export class FieldDropdown extends Field { prefix?: string; suffix?: string; } { - let hasImages = false; + let hasNonTextContent = false; const trimmedOptions = options.map((option): MenuOption => { - if (option === FieldDropdown.SEPARATOR) return option; + if (option === FieldDropdown.SEPARATOR) { + hasNonTextContent = true; + return option; + } const [label, value] = option; if (typeof label === 'string') { return [parsing.replaceMessageReferences(label), value]; } - hasImages = true; + hasNonTextContent = true; // Copy the image properties so they're not influenced by the original. // NOTE: No need to deep copy since image properties are only 1 level deep. const imageLabel = isImageProperties(label) ? {...label, alt: parsing.replaceMessageReferences(label.alt)} - : {...label}; + : label; return [imageLabel, value]; }); - if (hasImages || options.length < 2) return {options: trimmedOptions}; + if (hasNonTextContent || options.length < 2) { + return {options: trimmedOptions}; + } const stringOptions = trimmedOptions as [string, string][]; const stringLabels = stringOptions.map(([label]) => label); @@ -793,7 +798,7 @@ export class FieldDropdown extends Field { } else if (typeof option[1] !== 'string') { foundError = true; console.error( - `Invalid option[${i}]: Each FieldDropdown option id must be a string. + `Invalid option[${i}]: Each FieldDropdown option id must be a string. Found ${option[1]} in: ${option}`, ); } else if ( @@ -806,7 +811,7 @@ export class FieldDropdown extends Field { ) { foundError = true; console.error( - `Invalid option[${i}]: Each FieldDropdown option must have a string + `Invalid option[${i}]: Each FieldDropdown option must have a string label, image description, or HTML element. Found ${option[0]} in: ${option}`, ); } From c661dd1c946d12a41b2fc14b0d1deb5e3e3115f3 Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Mon, 28 Jul 2025 17:35:55 -0400 Subject: [PATCH 15/38] fix: dont save ids when copying blocks and comments (#9255) --- core/block_svg.ts | 1 + core/comments/rendered_workspace_comment.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/core/block_svg.ts b/core/block_svg.ts index 49b4a1ee6..1b85d38ce 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -962,6 +962,7 @@ export class BlockSvg blockState: blocks.save(this, { addCoordinates: true, addNextBlocks: false, + saveIds: false, }) as blocks.State, typeCounts: common.getBlockTypeCounts(this, true), }; diff --git a/core/comments/rendered_workspace_comment.ts b/core/comments/rendered_workspace_comment.ts index c4c1f3d4e..49c75e608 100644 --- a/core/comments/rendered_workspace_comment.ts +++ b/core/comments/rendered_workspace_comment.ts @@ -280,6 +280,7 @@ export class RenderedWorkspaceComment paster: WorkspaceCommentPaster.TYPE, commentState: commentSerialization.save(this, { addCoordinates: true, + saveIds: false, }), }; } From 8a578f5ce3b8e7e8f650369f1e9e19bcbb66e1ae Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 30 Jul 2025 08:58:30 -0700 Subject: [PATCH 16/38] fix: Allow overriding `VariableModel` via `options.plugins`. (#9257) --- core/registry.ts | 6 +++--- core/variable_map.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/registry.ts b/core/registry.ts index 2b00b775d..4980a5594 100644 --- a/core/registry.ts +++ b/core/registry.ts @@ -119,9 +119,9 @@ export class Type<_T> { /** @internal */ static PASTER = new Type>>('paster'); - static VARIABLE_MODEL = new Type>( - 'variableModel', - ); + static VARIABLE_MODEL = new Type< + IVariableModelStatic & IVariableModel + >('variableModel'); static VARIABLE_MAP = new Type>>( 'variableMap', diff --git a/core/variable_map.ts b/core/variable_map.ts index 3dd4bf547..ba36dcea6 100644 --- a/core/variable_map.ts +++ b/core/variable_map.ts @@ -255,9 +255,9 @@ export class VariableMap } const id = opt_id || idGenerator.genUid(); const type = opt_type || ''; - const VariableModel = registry.getObject( + const VariableModel = registry.getClassFromOptions( registry.Type.VARIABLE_MODEL, - registry.DEFAULT, + this.workspace.options, true, ); if (!VariableModel) { From c037e7d47ddd2c2a8ac51e6e648372adc0e14722 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 30 Jul 2025 09:48:00 -0700 Subject: [PATCH 17/38] fix: Scroll `CommentBarButton`s into view on selection. (#9259) --- core/keyboard_nav/line_cursor.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/keyboard_nav/line_cursor.ts b/core/keyboard_nav/line_cursor.ts index a301c3b37..c621e3a89 100644 --- a/core/keyboard_nav/line_cursor.ts +++ b/core/keyboard_nav/line_cursor.ts @@ -14,6 +14,7 @@ */ import {BlockSvg} from '../block_svg.js'; +import {CommentBarButton} from '../comments/comment_bar_button.js'; import {RenderedWorkspaceComment} from '../comments/rendered_workspace_comment.js'; import {Field} from '../field.js'; import {getFocusManager} from '../focus_manager.js'; @@ -403,6 +404,9 @@ export class LineCursor extends Marker { ); } else if (newNode instanceof RenderedWorkspaceComment) { newNode.workspace.scrollBoundsIntoView(newNode.getBoundingRectangle()); + } else if (newNode instanceof CommentBarButton) { + const comment = newNode.getParentComment(); + comment.workspace.scrollBoundsIntoView(comment.getBoundingRectangle()); } } From d9421892fb1b8815fc3092ea3c18e3444c3973df Mon Sep 17 00:00:00 2001 From: michaela-mm <63740955+michaela-mm@users.noreply.github.com> Date: Fri, 1 Aug 2025 20:30:23 +0200 Subject: [PATCH 18/38] fix: Check for existing event group in cleanUp() (#9265) --- core/workspace_svg.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 6c6b59301..af395b077 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -1674,7 +1674,10 @@ export class WorkspaceSvg /** Clean up the workspace by ordering all the blocks in a column such that none overlap. */ cleanUp() { this.setResizesEnabled(false); - eventUtils.setGroup(true); + const existingGroup = eventUtils.getGroup(); + if (!existingGroup) { + eventUtils.setGroup(true); + } const topBlocks = this.getTopBlocks(true); const movableBlocks = topBlocks.filter((block) => block.isMovable()); @@ -1722,7 +1725,7 @@ export class WorkspaceSvg block.getHeightWidth().height + minBlockHeight; } - eventUtils.setGroup(false); + eventUtils.setGroup(existingGroup); this.setResizesEnabled(true); } From 71256d69a0794e5a21bc8f1cf8c691fc3a898b9e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Aug 2025 12:22:09 +0000 Subject: [PATCH 19/38] chore(deps): bump eslint-plugin-jsdoc from 51.3.1 to 52.0.2 Bumps [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) from 51.3.1 to 52.0.2. - [Release notes](https://github.com/gajus/eslint-plugin-jsdoc/releases) - [Changelog](https://github.com/gajus/eslint-plugin-jsdoc/blob/main/.releaserc) - [Commits](https://github.com/gajus/eslint-plugin-jsdoc/compare/v51.3.1...v52.0.2) --- updated-dependencies: - dependency-name: eslint-plugin-jsdoc dependency-version: 52.0.2 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- package-lock.json | 9 +++++---- package.json | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 95e9a39b2..8d4d84a1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "eslint": "^9.15.0", "eslint-config-google": "^0.14.0", "eslint-config-prettier": "^10.1.1", - "eslint-plugin-jsdoc": "^51.3.1", + "eslint-plugin-jsdoc": "^52.0.2", "eslint-plugin-prettier": "^5.2.1", "glob": "^11.0.1", "globals": "^16.0.0", @@ -4088,10 +4088,11 @@ } }, "node_modules/eslint-plugin-jsdoc": { - "version": "51.3.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-51.3.1.tgz", - "integrity": "sha512-9v/e6XyrLf1HIs/uPCgm3GcUpH4BeuGVZJk7oauKKyS7su7d5Q6zx4Fq6TiYh+w7+b4Svy7ZWVCcNZJNx3y52w==", + "version": "52.0.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-52.0.2.tgz", + "integrity": "sha512-fYrnc7OpRifxxKjH78Y9/D/EouQDYD3G++bpR1Y+A+fy+CMzKZAdGIiHTIxCd2U10hb2y1NxN5TJt9aupq1vmw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@es-joy/jsdoccomment": "~0.52.0", "are-docs-informative": "^0.0.2", diff --git a/package.json b/package.json index 6ae18bd4a..3535ae06d 100644 --- a/package.json +++ b/package.json @@ -114,7 +114,7 @@ "eslint": "^9.15.0", "eslint-config-google": "^0.14.0", "eslint-config-prettier": "^10.1.1", - "eslint-plugin-jsdoc": "^51.3.1", + "eslint-plugin-jsdoc": "^52.0.2", "eslint-plugin-prettier": "^5.2.1", "glob": "^11.0.1", "globals": "^16.0.0", From 5e48e0db8c81262f45cf122a233afecbe41fe066 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Aug 2025 13:58:13 +0000 Subject: [PATCH 20/38] chore(deps): bump concurrently from 9.1.2 to 9.2.0 Bumps [concurrently](https://github.com/open-cli-tools/concurrently) from 9.1.2 to 9.2.0. - [Release notes](https://github.com/open-cli-tools/concurrently/releases) - [Commits](https://github.com/open-cli-tools/concurrently/compare/v9.1.2...v9.2.0) --- updated-dependencies: - dependency-name: concurrently dependency-version: 9.2.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 95e9a39b2..cbe2ca26e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3238,10 +3238,11 @@ } }, "node_modules/concurrently": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.2.tgz", - "integrity": "sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.0.tgz", + "integrity": "sha512-IsB/fiXTupmagMW4MNp2lx2cdSN2FfZq78vF90LBB+zZHArbIQZjQtzXCiXnvTxCZSvXanTqFLWBjw2UkLx1SQ==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^4.1.2", "lodash": "^4.17.21", From d5e347db44f0ef38d45f80be88505f73014b024e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Aug 2025 10:02:34 -0700 Subject: [PATCH 21/38] chore(deps): bump actions/first-interaction from 1 to 2 (#9274) Bumps [actions/first-interaction](https://github.com/actions/first-interaction) from 1 to 2. - [Release notes](https://github.com/actions/first-interaction/releases) - [Commits](https://github.com/actions/first-interaction/compare/v1...v2) --- updated-dependencies: - dependency-name: actions/first-interaction dependency-version: '2' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/welcome_new_contributors.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/welcome_new_contributors.yml b/.github/workflows/welcome_new_contributors.yml index 37ca9ef89..663f03205 100644 --- a/.github/workflows/welcome_new_contributors.yml +++ b/.github/workflows/welcome_new_contributors.yml @@ -9,7 +9,7 @@ jobs: permissions: pull-requests: write steps: - - uses: actions/first-interaction@v1 + - uses: actions/first-interaction@v2 with: repo-token: ${{ secrets.GITHUB_TOKEN }} pr-message: > From 683a4357ff158121c119d2e6d49b24e639263b7a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:13:43 -0700 Subject: [PATCH 22/38] chore(deps): bump gulp-rename from 2.0.0 to 2.1.0 (#9277) Bumps [gulp-rename](https://github.com/hparra/gulp-rename) from 2.0.0 to 2.1.0. - [Release notes](https://github.com/hparra/gulp-rename/releases) - [Changelog](https://github.com/hparra/gulp-rename/blob/master/CHANGELOG.md) - [Commits](https://github.com/hparra/gulp-rename/compare/v2.0.0...v2.1.0) --- updated-dependencies: - dependency-name: gulp-rename dependency-version: 2.1.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ce3036193..b1ff8dcbd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5536,10 +5536,11 @@ "dev": true }, "node_modules/gulp-rename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/gulp-rename/-/gulp-rename-2.0.0.tgz", - "integrity": "sha512-97Vba4KBzbYmR5VBs9mWmK+HwIf5mj+/zioxfZhOKeXtx5ZjBk57KFlePf5nxq9QsTtFl0ejnHE3zTC9MHXqyQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/gulp-rename/-/gulp-rename-2.1.0.tgz", + "integrity": "sha512-dGuzuH8jQGqCMqC544IEPhs5+O2l+IkdoSZsgd4kY97M1CxQeI3qrmweQBIrxLBbjbe/8uEWK8HHcNBc3OCy4g==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } From f24940e7816891ee78add455d37edbdf7f17ab23 Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Mon, 4 Aug 2025 16:14:44 -0400 Subject: [PATCH 23/38] fix: dont add comments to full block fields (#9263) * fix: dont add comments to full block fields * chore: remove some nonnull assertions --- core/block.ts | 2 +- core/contextmenu_items.ts | 23 +++++++++++++++++------ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/core/block.ts b/core/block.ts index 9f7c11d4f..8ba61b1ca 100644 --- a/core/block.ts +++ b/core/block.ts @@ -1118,7 +1118,7 @@ export class Block { * * @yields A generator that can be used to iterate the fields on the block. */ - *getFields(): Generator { + *getFields(): Generator { for (const input of this.inputList) { for (const field of input.fieldRow) { yield field; diff --git a/core/contextmenu_items.ts b/core/contextmenu_items.ts index 774bfdde2..8bb71775f 100644 --- a/core/contextmenu_items.ts +++ b/core/contextmenu_items.ts @@ -25,6 +25,12 @@ import {StatementInput} from './renderers/zelos/zelos.js'; import {Coordinate} from './utils/coordinate.js'; import type {WorkspaceSvg} from './workspace_svg.js'; +function isFullBlockField(block?: BlockSvg) { + if (!block || !block.isSimpleReporter()) return false; + const firstField = block.getFields().next().value; + return firstField?.isFullBlockField(); +} + /** * Option to undo previous action. */ @@ -362,10 +368,15 @@ export function registerComment() { preconditionFn(scope: Scope) { const block = scope.block; if ( - !block!.isInFlyout && - block!.workspace.options.comments && - !block!.isCollapsed() && - block!.isEditable() + block && + !block.isInFlyout && + block.workspace.options.comments && + !block.isCollapsed() && + block.isEditable() && + // Either block already has a comment so let us remove it, + // or the block isn't just one full-block field block, which + // shouldn't be allowed to have comments as there's no way to read them. + (block.hasIcon(CommentIcon.TYPE) || !isFullBlockField(block)) ) { return 'enabled'; } @@ -373,8 +384,8 @@ export function registerComment() { }, callback(scope: Scope) { const block = scope.block; - if (block!.hasIcon(CommentIcon.TYPE)) { - block!.setCommentText(null); + if (block && block.hasIcon(CommentIcon.TYPE)) { + block.setCommentText(null); } else { block!.setCommentText(''); } From 88151fcadd00a5812efa67e9221def35da353f93 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Mon, 4 Aug 2025 13:18:31 -0700 Subject: [PATCH 24/38] fix: Display focused icons without transparency. (#9268) --- core/css.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/css.ts b/core/css.ts index 4f4a4daaf..30ee47fc5 100644 --- a/core/css.ts +++ b/core/css.ts @@ -241,7 +241,7 @@ let content = ` cursor: default; } -.blocklyIconGroup:not(:hover), +.blocklyIconGroup:not(:hover):not(:focus), .blocklyIconGroupReadonly { opacity: .6; } From 44e78b1456f969c2050099800be82a97e87433d7 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 5 Aug 2025 11:17:10 -0700 Subject: [PATCH 25/38] feat: Add an option to copy subsequent blocks when getting copy data from a block. (#9279) --- core/block_svg.ts | 7 +++++-- tests/mocha/clipboard_test.js | 25 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/core/block_svg.ts b/core/block_svg.ts index 1b85d38ce..c6065282a 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -951,9 +951,12 @@ export class BlockSvg /** * Encode a block for copying. * + * @param addNextBlocks If true, copy subsequent blocks attached to this one + * as well. + * * @returns Copy metadata, or null if the block is an insertion marker. */ - toCopyData(): BlockCopyData | null { + toCopyData(addNextBlocks = false): BlockCopyData | null { if (this.isInsertionMarker_) { return null; } @@ -961,7 +964,7 @@ export class BlockSvg paster: BlockPaster.TYPE, blockState: blocks.save(this, { addCoordinates: true, - addNextBlocks: false, + addNextBlocks, saveIds: false, }) as blocks.State, typeCounts: common.getBlockTypeCounts(this, true), diff --git a/tests/mocha/clipboard_test.js b/tests/mocha/clipboard_test.js index 85cdd2297..d58f78b9b 100644 --- a/tests/mocha/clipboard_test.js +++ b/tests/mocha/clipboard_test.js @@ -61,6 +61,31 @@ suite('Clipboard', function () { ); }); + test('pasting blocks includes next blocks if requested', function () { + const block = Blockly.serialization.blocks.append( + { + 'type': 'controls_if', + 'id': 'blockId', + 'next': { + 'block': { + 'type': 'controls_if', + 'id': 'blockId2', + }, + }, + }, + this.workspace, + ); + assert.equal(this.workspace.getBlocksByType('controls_if').length, 2); + // Both blocks should be copied + const data = block.toCopyData(true); + this.clock.runAll(); + + Blockly.clipboard.paste(data, this.workspace); + this.clock.runAll(); + // After pasting, we should have gone from 2 to 4 blocks. + assert.equal(this.workspace.getBlocksByType('controls_if').length, 4); + }); + test('copied from a mutator pastes them into the mutator', async function () { const block = Blockly.serialization.blocks.append( { From af57a3eaa387a4ebe374332eed912a1124639e3c Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 6 Aug 2025 12:45:17 -0700 Subject: [PATCH 26/38] refactor: Make `InsertionMarkerPreviewer`'s block serialization amenable to subclassing. (#9282) --- core/insertion_marker_previewer.ts | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/core/insertion_marker_previewer.ts b/core/insertion_marker_previewer.ts index 2343b9adc..8b5b82468 100644 --- a/core/insertion_marker_previewer.ts +++ b/core/insertion_marker_previewer.ts @@ -150,8 +150,17 @@ export class InsertionMarkerPreviewer implements IConnectionPreviewer { return markerConn; } - private createInsertionMarker(origBlock: BlockSvg) { - const blockJson = blocks.save(origBlock, { + /** + * Transforms the given block into a JSON representation used to construct an + * insertion marker. + * + * @param block The block to serialize and use as an insertion marker. + * @returns A JSON-formatted string corresponding to a serialized + * representation of the given block suitable for use as an insertion + * marker. + */ + protected serializeBlockToInsertionMarker(block: BlockSvg) { + const blockJson = blocks.save(block, { addCoordinates: false, addInputBlocks: false, addNextBlocks: false, @@ -160,10 +169,15 @@ export class InsertionMarkerPreviewer implements IConnectionPreviewer { if (!blockJson) { throw new Error( - `Failed to serialize source block. ${origBlock.toDevString()}`, + `Failed to serialize source block. ${block.toDevString()}`, ); } + return blockJson; + } + + private createInsertionMarker(origBlock: BlockSvg) { + const blockJson = this.serializeBlockToInsertionMarker(origBlock); const result = blocks.append(blockJson, this.workspace) as BlockSvg; // Turn shadow blocks that are created programmatically during From 62f3b8914a1d3dfbb1f4a64c41adba56957663d5 Mon Sep 17 00:00:00 2001 From: Maribeth Moffatt Date: Wed, 6 Aug 2025 17:01:59 -0400 Subject: [PATCH 27/38] chore: add tests for clipboard (#9254) * chore: add tests for clipboard * chore: clean up --- tests/browser/test/clipboard_test.mjs | 611 ++++++++++++++++++++++++++ tests/browser/test/test_setup.mjs | 28 ++ 2 files changed, 639 insertions(+) create mode 100644 tests/browser/test/clipboard_test.mjs diff --git a/tests/browser/test/clipboard_test.mjs b/tests/browser/test/clipboard_test.mjs new file mode 100644 index 000000000..37dd359d3 --- /dev/null +++ b/tests/browser/test/clipboard_test.mjs @@ -0,0 +1,611 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as chai from 'chai'; +import {Key} from 'webdriverio'; +import { + PAUSE_TIME, + clickWorkspace, + focusOnBlock, + getAllBlocks, + getBlockTypeFromWorkspace, + getCategory, + getSelectedBlockId, + getSelectedBlockType, + openMutatorForBlock, + testFileLocations, + testSetup, +} from './test_setup.mjs'; + +const testBlockJson = { + 'blocks': { + 'languageVersion': 0, + 'blocks': [ + { + 'type': 'controls_repeat_ext', + 'id': 'controls_repeat_1', + 'x': 88, + 'y': 88, + 'inputs': { + 'TIMES': { + 'shadow': { + 'type': 'math_number', + 'id': 'math_number_shadow_1', + 'fields': { + 'NUM': 10, + }, + }, + }, + 'DO': { + 'block': { + 'type': 'controls_if', + 'id': 'controls_if_1', + 'inputs': { + 'IF0': { + 'block': { + 'type': 'logic_boolean', + 'id': 'logic_boolean_1', + 'fields': { + 'BOOL': 'TRUE', + }, + }, + }, + 'DO0': { + 'block': { + 'type': 'text_print', + 'id': 'text_print_1', + 'inputs': { + 'TEXT': { + 'shadow': { + 'type': 'text', + 'id': 'text_shadow_1', + 'fields': { + 'TEXT': 'abc', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + ], + }, +}; + +async function loadStartBlocks(browser) { + await browser.execute((stringifiedJson) => { + // Hangs forever if the json isn't stringified ¯\_(ツ)_/¯ + const testBlockJson = JSON.parse(stringifiedJson); + const workspace = Blockly.common.getMainWorkspace(); + Blockly.serialization.workspaces.load(testBlockJson, workspace); + }, JSON.stringify(testBlockJson)); + await browser.pause(PAUSE_TIME); +} + +suite('Clipboard test', async function () { + // Setting timeout to unlimited as these tests take longer time to run + this.timeout(0); + + // Clear the workspace and load start blocks + setup(async function () { + this.browser = await testSetup(testFileLocations.PLAYGROUND); + await this.browser.pause(PAUSE_TIME); + }); + + test('Paste block to/from main workspace', async function () { + await loadStartBlocks(this.browser); + // Select and copy the "true" block + await focusOnBlock(this.browser, 'logic_boolean_1'); + await this.browser.pause(PAUSE_TIME); + + await this.browser.keys([Key.Ctrl, 'c']); + await this.browser.pause(PAUSE_TIME); + + // Check how many blocks there are before pasting + const allBlocksBeforePaste = await getAllBlocks(this.browser); + + // Paste the block while still in the main workspace + await this.browser.keys([Key.Ctrl, 'v']); + await this.browser.pause(PAUSE_TIME); + + // Check result + const allBlocksAfterPaste = await getAllBlocks(this.browser); + chai.assert.equal( + allBlocksAfterPaste.length, + allBlocksBeforePaste.length + 1, + 'Expected there to be one additional block after paste', + ); + const focusedBlockId = await getSelectedBlockId(this.browser); + chai.assert.notEqual( + focusedBlockId, + 'logic_boolean_1', + 'Newly pasted block should be selected', + ); + const focusedBlockType = await getSelectedBlockType(this.browser); + chai.assert.equal( + focusedBlockType, + 'logic_boolean', + 'Newly pasted block should be selected', + ); + }); + + test('Copying a block also copies and pastes its children', async function () { + await loadStartBlocks(this.browser); + // Select and copy the "if/else" block which has children + await focusOnBlock(this.browser, 'controls_if_1'); + await this.browser.pause(PAUSE_TIME); + + await this.browser.keys([Key.Ctrl, 'c']); + await this.browser.pause(PAUSE_TIME); + + // Check how many blocks there are before pasting + const allBlocksBeforePaste = await getAllBlocks(this.browser); + + // Paste the block while still in the main workspace + await this.browser.keys([Key.Ctrl, 'v']); + await this.browser.pause(PAUSE_TIME); + + // Check result + const allBlocksAfterPaste = await getAllBlocks(this.browser); + chai.assert.equal( + allBlocksAfterPaste.length, + allBlocksBeforePaste.length + 4, + 'Expected there to be four additional blocks after paste', + ); + }); + + test('Paste shadow block to/from main workspace', async function () { + await loadStartBlocks(this.browser); + // Select and copy the shadow number block + await focusOnBlock(this.browser, 'math_number_shadow_1'); + await this.browser.pause(PAUSE_TIME); + + await this.browser.keys([Key.Ctrl, 'c']); + await this.browser.pause(PAUSE_TIME); + + // Check how many blocks there are before pasting + const allBlocksBeforePaste = await getAllBlocks(this.browser); + + // Paste the block while still in the main workspace + await this.browser.keys([Key.Ctrl, 'v']); + await this.browser.pause(PAUSE_TIME); + + // Check result + const allBlocksAfterPaste = await getAllBlocks(this.browser); + chai.assert.equal( + allBlocksAfterPaste.length, + allBlocksBeforePaste.length + 1, + 'Expected there to be one additional block after paste', + ); + const focusedBlockId = await getSelectedBlockId(this.browser); + chai.assert.notEqual( + focusedBlockId, + 'math_number_shadow_1', + 'Newly pasted block should be selected', + ); + const focusedBlockType = await getSelectedBlockType(this.browser); + chai.assert.equal( + focusedBlockType, + 'math_number', + 'Newly pasted block should be selected', + ); + const focusedBlockIsShadow = await this.browser.execute(() => { + return Blockly.common.getSelected().isShadow(); + }); + chai.assert.isFalse( + focusedBlockIsShadow, + 'Expected the pasted version of the block to not be a shadow block', + ); + }); + + test('Copy block from flyout, paste to main workspace', async function () { + // Open flyout + await getCategory(this.browser, 'Logic').then((category) => + category.click(), + ); + + // Focus on first block in flyout + await this.browser.execute(() => { + const ws = Blockly.getMainWorkspace().getFlyout().getWorkspace(); + const block = ws.getBlocksByType('controls_if')[0]; + Blockly.getFocusManager().focusNode(block); + }); + await this.browser.pause(PAUSE_TIME); + + // Copy + await this.browser.keys([Key.Ctrl, 'c']); + await this.browser.pause(PAUSE_TIME); + + // Select the main workspace + await clickWorkspace(this.browser); + await this.browser.pause(PAUSE_TIME); + + // Paste + await this.browser.keys([Key.Ctrl, 'v']); + await this.browser.pause(PAUSE_TIME); + + // Check that the block is now on the workspace and selected + const allBlocks = await getAllBlocks(this.browser); + chai.assert.equal( + allBlocks.length, + 1, + 'Expected there to be one block on main workspace after paste from flyout', + ); + + const focusedBlockType = await getSelectedBlockType(this.browser); + chai.assert.equal( + focusedBlockType, + 'controls_if', + 'Newly pasted block should be selected', + ); + }); + + test('Copy block from flyout, paste while flyout focused', async function () { + // Open flyout + await getCategory(this.browser, 'Logic').then((category) => + category.click(), + ); + + // Focus on first block in flyout + await this.browser.execute(() => { + const ws = Blockly.getMainWorkspace().getFlyout().getWorkspace(); + const block = ws.getBlocksByType('controls_if')[0]; + Blockly.getFocusManager().focusNode(block); + }); + await this.browser.pause(PAUSE_TIME); + + // Copy + await this.browser.keys([Key.Ctrl, 'c']); + await this.browser.pause(PAUSE_TIME); + + // Paste + await this.browser.keys([Key.Ctrl, 'v']); + await this.browser.pause(PAUSE_TIME); + + // Check that the flyout is closed + const flyoutIsVisible = await this.browser + .$('.blocklyToolboxFlyout') + .then((elem) => elem.isDisplayed()); + chai.assert.isFalse(flyoutIsVisible, 'Expected flyout to not be open'); + + // Check that the block is now on the main workspace and selected + const allBlocks = await getAllBlocks(this.browser); + chai.assert.equal( + allBlocks.length, + 1, + 'Expected there to be one block on main workspace after paste from flyout', + ); + + const focusedBlockType = await getSelectedBlockType(this.browser); + chai.assert.equal( + focusedBlockType, + 'controls_if', + 'Newly pasted block should be selected', + ); + }); + + test('Copy block from mutator flyout, paste to mutator workspace', async function () { + // Load the start blocks + await loadStartBlocks(this.browser); + + // Open the controls_if mutator + const block = await getBlockTypeFromWorkspace( + this.browser, + 'controls_if', + 0, + ); + await openMutatorForBlock(this.browser, block); + + // Select the first block in the mutator flyout + await this.browser.execute( + (blockId, mutatorBlockType) => { + const flyoutBlock = Blockly.getMainWorkspace() + .getBlockById(blockId) + .mutator.getWorkspace() + .getFlyout() + .getWorkspace() + .getBlocksByType(mutatorBlockType)[0]; + + Blockly.getFocusManager().focusNode(flyoutBlock); + }, + 'controls_if_1', + 'controls_if_elseif', + ); + await this.browser.pause(PAUSE_TIME); + + // Copy + await this.browser.keys([Key.Ctrl, 'c']); + await this.browser.pause(PAUSE_TIME); + + // Paste + await this.browser.keys([Key.Ctrl, 'v']); + await this.browser.pause(PAUSE_TIME); + + // Check that the block is now in the mutator workspace and selected + const numberOfIfElseBlocks = await this.browser.execute( + (blockId, mutatorBlockType) => { + return Blockly.getMainWorkspace() + .getBlockById(blockId) + .mutator.getWorkspace() + .getBlocksByType(mutatorBlockType).length; + }, + 'controls_if_1', + 'controls_if_elseif', + ); + + chai.assert.equal( + numberOfIfElseBlocks, + 1, + 'Expected there to be one if_else block in mutator workspace', + ); + + const focusedBlockType = await getSelectedBlockType(this.browser); + chai.assert.equal( + focusedBlockType, + 'controls_if_elseif', + 'Newly pasted block should be selected', + ); + }); + + test('Copy block from mutator flyout, paste to main workspace while mutator open', async function () { + // Load the start blocks + await loadStartBlocks(this.browser); + + // Open the controls_if mutator + const block = await getBlockTypeFromWorkspace( + this.browser, + 'controls_if', + 0, + ); + await openMutatorForBlock(this.browser, block); + + // Select the first block in the mutator flyout + await this.browser.execute( + (blockId, mutatorBlockType) => { + const flyoutBlock = Blockly.getMainWorkspace() + .getBlockById(blockId) + .mutator.getWorkspace() + .getFlyout() + .getWorkspace() + .getBlocksByType(mutatorBlockType)[0]; + + Blockly.getFocusManager().focusNode(flyoutBlock); + }, + 'controls_if_1', + 'controls_if_elseif', + ); + await this.browser.pause(PAUSE_TIME); + + // Copy + await this.browser.keys([Key.Ctrl, 'c']); + await this.browser.pause(PAUSE_TIME); + + // Click the main workspace + await clickWorkspace(this.browser); + + // Paste + await this.browser.keys([Key.Ctrl, 'v']); + await this.browser.pause(PAUSE_TIME); + + // Check that the block is now in the mutator workspace and selected + const numberOfIfElseBlocks = await this.browser.execute( + (blockId, mutatorBlockType) => { + return Blockly.getMainWorkspace() + .getBlockById(blockId) + .mutator.getWorkspace() + .getBlocksByType(mutatorBlockType).length; + }, + 'controls_if_1', + 'controls_if_elseif', + ); + + chai.assert.equal( + numberOfIfElseBlocks, + 1, + 'Expected there to be one if_else block in mutator workspace', + ); + + const focusedBlockType = await getSelectedBlockType(this.browser); + chai.assert.equal( + focusedBlockType, + 'controls_if_elseif', + 'Newly pasted block should be selected', + ); + + // Check that there are no new blocks on the main workspace + const numberOfIfElseBlocksOnMainWorkspace = await this.browser.execute( + (mutatorBlockType) => { + return Blockly.getMainWorkspace().getBlocksByType(mutatorBlockType) + .length; + }, + 'controls_if_elseif', + ); + chai.assert.equal( + numberOfIfElseBlocksOnMainWorkspace, + 0, + 'Mutator blocks should not appear on main workspace', + ); + }); + + test('Copy block from mutator flyout, paste to main workspace while mutator closed', async function () { + // Load the start blocks + await loadStartBlocks(this.browser); + + // Open the controls_if mutator + const block = await getBlockTypeFromWorkspace( + this.browser, + 'controls_if', + 0, + ); + await openMutatorForBlock(this.browser, block); + + // Select the first block in the mutator flyout + await this.browser.execute( + (blockId, mutatorBlockType) => { + const flyoutBlock = Blockly.getMainWorkspace() + .getBlockById(blockId) + .mutator.getWorkspace() + .getFlyout() + .getWorkspace() + .getBlocksByType(mutatorBlockType)[0]; + + Blockly.getFocusManager().focusNode(flyoutBlock); + }, + 'controls_if_1', + 'controls_if_elseif', + ); + await this.browser.pause(PAUSE_TIME); + + // Copy + await this.browser.keys([Key.Ctrl, 'c']); + await this.browser.pause(PAUSE_TIME); + + // Close the mutator flyout (calling this method on open mutator closes it) + await openMutatorForBlock(this.browser, block); + + // Click the main workspace + await clickWorkspace(this.browser); + + // Paste + await this.browser.keys([Key.Ctrl, 'v']); + await this.browser.pause(PAUSE_TIME); + + // Check that there are no new blocks on the main workspace + const numberOfIfElseBlocksOnMainWorkspace = await this.browser.execute( + (mutatorBlockType) => { + return Blockly.getMainWorkspace().getBlocksByType(mutatorBlockType) + .length; + }, + 'controls_if_elseif', + ); + chai.assert.equal( + numberOfIfElseBlocksOnMainWorkspace, + 0, + 'Mutator blocks should not appear on main workspace', + ); + }); + + test('Copy workspace comment, paste to main workspace', async function () { + // Add a workspace comment to the workspace + await this.browser.execute(() => { + const workspace = Blockly.getMainWorkspace(); + const json = { + 'workspaceComments': [ + { + 'height': 100, + 'width': 120, + 'id': 'workspace_comment_1', + 'x': 13, + 'y': -12, + 'text': 'This is a comment', + }, + ], + }; + Blockly.serialization.workspaces.load(json, workspace); + }); + await this.browser.pause(PAUSE_TIME); + + // Select the workspace comment + await this.browser.execute(() => { + const comment = Blockly.getMainWorkspace().getCommentById( + 'workspace_comment_1', + ); + Blockly.getFocusManager().focusNode(comment); + }); + await this.browser.pause(PAUSE_TIME); + + // Copy + await this.browser.keys([Key.Ctrl, 'c']); + await this.browser.pause(PAUSE_TIME); + + // Click the main workspace + await clickWorkspace(this.browser); + + // Paste + await this.browser.keys([Key.Ctrl, 'v']); + await this.browser.pause(PAUSE_TIME); + + // Check that there are 2 comments on the workspace + const numberOfComments = await this.browser.execute(() => { + return Blockly.getMainWorkspace().getTopComments().length; + }); + chai.assert.equal( + numberOfComments, + 2, + 'Expected 2 workspace comments after pasting', + ); + }); + + test('Cut block from main workspace, paste to main workspace', async function () { + await loadStartBlocks(this.browser); + // Select and cut the "true" block + await focusOnBlock(this.browser, 'logic_boolean_1'); + await this.browser.pause(PAUSE_TIME); + + await this.browser.keys([Key.Ctrl, 'x']); + await this.browser.pause(PAUSE_TIME); + + // Check that the "true" block was deleted + const trueBlock = await this.browser.execute(() => { + return Blockly.getMainWorkspace().getBlockById('logic_boolean_1') ?? null; + }); + chai.assert.isNull(trueBlock); + + // Check how many blocks there are before pasting + const allBlocksBeforePaste = await getAllBlocks(this.browser); + + // Paste the block while still in the main workspace + await this.browser.keys([Key.Ctrl, 'v']); + await this.browser.pause(PAUSE_TIME); + + // Check result + const allBlocksAfterPaste = await getAllBlocks(this.browser); + chai.assert.equal( + allBlocksAfterPaste.length, + allBlocksBeforePaste.length + 1, + 'Expected there to be one additional block after paste', + ); + }); + + test('Cannot cut block from flyout', async function () { + // Open flyout + await getCategory(this.browser, 'Logic').then((category) => + category.click(), + ); + + // Focus on first block in flyout + await this.browser.execute(() => { + const ws = Blockly.getMainWorkspace().getFlyout().getWorkspace(); + const block = ws.getBlocksByType('controls_if')[0]; + Blockly.getFocusManager().focusNode(block); + }); + await this.browser.pause(PAUSE_TIME); + + // Cut + await this.browser.keys([Key.Ctrl, 'x']); + await this.browser.pause(PAUSE_TIME); + + // Select the main workspace + await clickWorkspace(this.browser); + await this.browser.pause(PAUSE_TIME); + + // Paste + await this.browser.keys([Key.Ctrl, 'v']); + await this.browser.pause(PAUSE_TIME); + + // Check that no block was pasted + const allBlocks = await getAllBlocks(this.browser); + chai.assert.equal( + allBlocks.length, + 0, + 'Expected no blocks in the workspace because nothing to paste', + ); + }); +}); diff --git a/tests/browser/test/test_setup.mjs b/tests/browser/test/test_setup.mjs index 6cf4986fc..0a8998c3e 100644 --- a/tests/browser/test/test_setup.mjs +++ b/tests/browser/test/test_setup.mjs @@ -127,6 +127,23 @@ export const screenDirection = { LTR: 1, }; +/** + * Focuses and selects a block with the provided ID. + * + * This throws an error if no block exists for the specified ID. + * + * @param browser The active WebdriverIO Browser object. + * @param blockId The ID of the block to select. + */ +export async function focusOnBlock(browser, blockId) { + return await browser.execute((blockId) => { + const workspaceSvg = Blockly.getMainWorkspace(); + const block = workspaceSvg.getBlockById(blockId); + if (!block) throw new Error(`No block found with ID: ${blockId}.`); + Blockly.getFocusManager().focusNode(block); + }, blockId); +} + /** * @param browser The active WebdriverIO Browser object. * @return A Promise that resolves to the ID of the currently selected block. @@ -138,6 +155,17 @@ export async function getSelectedBlockId(browser) { }); } +/** + * @param browser The active WebdriverIO Browser object. + * @return A Promise that resolves to the ID of the currently selected block. + */ +export async function getSelectedBlockType(browser) { + return await browser.execute(() => { + // Note: selected is an ICopyable and I am assuming that it is a BlockSvg. + return Blockly.common.getSelected()?.type; + }); +} + /** * @param browser The active WebdriverIO Browser object. * @return A Promise that resolves to the selected block's root SVG element, From f9d0ec9d24d6ee78a0e4a52327dbd0e1688b7f1b Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 6 Aug 2025 14:04:12 -0700 Subject: [PATCH 28/38] refactor: Associate comment bar buttons with the comment view. (#9278) --- core/comments/collapse_comment_bar_button.ts | 9 +++++---- core/comments/comment_bar_button.ts | 16 +++++----------- core/comments/comment_view.ts | 11 ++++++++--- core/comments/delete_comment_bar_button.ts | 6 ++++-- .../comment_bar_button_navigation_policy.ts | 8 +++++--- core/keyboard_nav/line_cursor.ts | 13 +++++++++++-- 6 files changed, 38 insertions(+), 25 deletions(-) diff --git a/core/comments/collapse_comment_bar_button.ts b/core/comments/collapse_comment_bar_button.ts index b0738d707..304e2af81 100644 --- a/core/comments/collapse_comment_bar_button.ts +++ b/core/comments/collapse_comment_bar_button.ts @@ -10,6 +10,7 @@ import * as dom from '../utils/dom.js'; import {Svg} from '../utils/svg.js'; import type {WorkspaceSvg} from '../workspace_svg.js'; import {CommentBarButton} from './comment_bar_button.js'; +import type {CommentView} from './comment_view.js'; /** * Magic string appended to the comment ID to create a unique ID for this button. @@ -42,8 +43,9 @@ export class CollapseCommentBarButton extends CommentBarButton { protected readonly id: string, protected readonly workspace: WorkspaceSvg, protected readonly container: SVGGElement, + protected readonly commentView: CommentView, ) { - super(id, workspace, container); + super(id, workspace, container, commentView); this.icon = dom.createSvgElement( Svg.IMAGE, @@ -86,14 +88,13 @@ export class CollapseCommentBarButton extends CommentBarButton { override performAction(e?: Event) { touch.clearTouchIdentifier(); - const comment = this.getParentComment(); - comment.view.bringToFront(); + this.getCommentView().bringToFront(); if (e && e instanceof PointerEvent && browserEvents.isRightButton(e)) { e.stopPropagation(); return; } - comment.setCollapsed(!comment.isCollapsed()); + this.getCommentView().setCollapsed(!this.getCommentView().isCollapsed()); this.workspace.hideChaff(); e?.stopPropagation(); diff --git a/core/comments/comment_bar_button.ts b/core/comments/comment_bar_button.ts index d78a7fd86..24a084ad2 100644 --- a/core/comments/comment_bar_button.ts +++ b/core/comments/comment_bar_button.ts @@ -7,7 +7,7 @@ import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; import {Rect} from '../utils/rect.js'; import type {WorkspaceSvg} from '../workspace_svg.js'; -import type {RenderedWorkspaceComment} from './rendered_workspace_comment.js'; +import type {CommentView} from './comment_view.js'; /** * Button displayed on a comment's top bar. @@ -29,6 +29,7 @@ export abstract class CommentBarButton implements IFocusableNode { protected readonly id: string, protected readonly workspace: WorkspaceSvg, protected readonly container: SVGGElement, + protected readonly commentView: CommentView, ) {} /** @@ -39,17 +40,10 @@ export abstract class CommentBarButton implements IFocusableNode { } /** - * Returns the parent comment of this comment bar button. + * Returns the parent comment view of this comment bar button. */ - getParentComment(): RenderedWorkspaceComment { - const comment = this.workspace.getCommentById(this.id); - if (!comment) { - throw new Error( - `Comment bar button ${this.id} has no corresponding comment`, - ); - } - - return comment; + getCommentView(): CommentView { + return this.commentView; } /** Adjusts the position of this button within its parent container. */ diff --git a/core/comments/comment_view.ts b/core/comments/comment_view.ts index 936d74650..ca0c261c3 100644 --- a/core/comments/comment_view.ts +++ b/core/comments/comment_view.ts @@ -102,7 +102,7 @@ export class CommentView implements IRenderedElement { constructor( readonly workspace: WorkspaceSvg, - private commentId: string, + readonly commentId: string, ) { this.svgRoot = dom.createSvgElement(Svg.G, { 'class': 'blocklyComment blocklyEditable blocklyDraggable', @@ -176,12 +176,18 @@ export class CommentView implements IRenderedElement { this.commentId, this.workspace, topBarGroup, + this, ); const foldoutButton = new CollapseCommentBarButton( this.commentId, this.workspace, topBarGroup, + this, ); + this.addDisposeListener(() => { + deleteButton.dispose(); + foldoutButton.dispose(); + }); const textPreview = dom.createSvgElement( Svg.TEXT, { @@ -612,13 +618,12 @@ export class CommentView implements IRenderedElement { /** Disposes of this comment view. */ dispose() { this.disposing = true; - this.foldoutButton.dispose(); - this.deleteButton.dispose(); dom.removeNode(this.svgRoot); // Loop through listeners backwards in case they remove themselves. for (let i = this.disposeListeners.length - 1; i >= 0; i--) { this.disposeListeners[i](); } + this.disposeListeners.length = 0; this.disposed = true; } diff --git a/core/comments/delete_comment_bar_button.ts b/core/comments/delete_comment_bar_button.ts index 0b7dcd0ea..c61db9b9c 100644 --- a/core/comments/delete_comment_bar_button.ts +++ b/core/comments/delete_comment_bar_button.ts @@ -11,6 +11,7 @@ import * as dom from '../utils/dom.js'; import {Svg} from '../utils/svg.js'; import type {WorkspaceSvg} from '../workspace_svg.js'; import {CommentBarButton} from './comment_bar_button.js'; +import type {CommentView} from './comment_view.js'; /** * Magic string appended to the comment ID to create a unique ID for this button. @@ -42,8 +43,9 @@ export class DeleteCommentBarButton extends CommentBarButton { protected readonly id: string, protected readonly workspace: WorkspaceSvg, protected readonly container: SVGGElement, + protected readonly commentView: CommentView, ) { - super(id, workspace, container); + super(id, workspace, container, commentView); this.icon = dom.createSvgElement( Svg.IMAGE, @@ -97,7 +99,7 @@ export class DeleteCommentBarButton extends CommentBarButton { return; } - this.getParentComment().dispose(); + this.getCommentView().dispose(); e?.stopPropagation(); getFocusManager().focusNode(this.workspace); } diff --git a/core/keyboard_nav/comment_bar_button_navigation_policy.ts b/core/keyboard_nav/comment_bar_button_navigation_policy.ts index f676f4655..6654d2d8f 100644 --- a/core/keyboard_nav/comment_bar_button_navigation_policy.ts +++ b/core/keyboard_nav/comment_bar_button_navigation_policy.ts @@ -31,7 +31,9 @@ export class CommentBarButtonNavigationPolicy * @returns The parent comment of the given CommentBarButton. */ getParent(current: CommentBarButton): IFocusableNode | null { - return current.getParentComment(); + return current + .getCommentView() + .workspace.getCommentById(current.getCommentView().commentId); } /** @@ -41,7 +43,7 @@ export class CommentBarButtonNavigationPolicy * @returns The next CommentBarButton, if any. */ getNextSibling(current: CommentBarButton): IFocusableNode | null { - const children = current.getParentComment().view.getCommentBarButtons(); + const children = current.getCommentView().getCommentBarButtons(); const currentIndex = children.indexOf(current); if (currentIndex >= 0 && currentIndex + 1 < children.length) { return children[currentIndex + 1]; @@ -56,7 +58,7 @@ export class CommentBarButtonNavigationPolicy * @returns The CommentBarButton's previous CommentBarButton, if any. */ getPreviousSibling(current: CommentBarButton): IFocusableNode | null { - const children = current.getParentComment().view.getCommentBarButtons(); + const children = current.getCommentView().getCommentBarButtons(); const currentIndex = children.indexOf(current); if (currentIndex > 0) { return children[currentIndex - 1]; diff --git a/core/keyboard_nav/line_cursor.ts b/core/keyboard_nav/line_cursor.ts index c621e3a89..13e5a729d 100644 --- a/core/keyboard_nav/line_cursor.ts +++ b/core/keyboard_nav/line_cursor.ts @@ -20,6 +20,7 @@ import {Field} from '../field.js'; import {getFocusManager} from '../focus_manager.js'; import type {IFocusableNode} from '../interfaces/i_focusable_node.js'; import * as registry from '../registry.js'; +import {Rect} from '../utils/rect.js'; import {WorkspaceSvg} from '../workspace_svg.js'; import {Marker} from './marker.js'; @@ -405,8 +406,16 @@ export class LineCursor extends Marker { } else if (newNode instanceof RenderedWorkspaceComment) { newNode.workspace.scrollBoundsIntoView(newNode.getBoundingRectangle()); } else if (newNode instanceof CommentBarButton) { - const comment = newNode.getParentComment(); - comment.workspace.scrollBoundsIntoView(comment.getBoundingRectangle()); + const commentView = newNode.getCommentView(); + const xy = commentView.getRelativeToSurfaceXY(); + const size = commentView.getSize(); + const bounds = new Rect( + xy.y, + xy.y + size.height, + xy.x, + xy.x + size.width, + ); + commentView.workspace.scrollBoundsIntoView(bounds); } } From 7d1d745416621cc352c1048e6fb06c9f611cbcc4 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 6 Aug 2025 14:08:01 -0700 Subject: [PATCH 29/38] fix: Drag immovable and shadow blocks along with their parent. (#9281) --- core/block.ts | 42 ++++++++++++++++++++++++--------------- tests/mocha/block_test.js | 29 +++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 16 deletions(-) diff --git a/core/block.ts b/core/block.ts index 8ba61b1ca..d15700a7e 100644 --- a/core/block.ts +++ b/core/block.ts @@ -501,22 +501,32 @@ export class Block { // Detach this block from the parent's tree. this.previousConnection.disconnect(); } - const nextBlock = this.getNextBlock(); - if (opt_healStack && nextBlock && !nextBlock.isShadow()) { - // Disconnect the next statement. - const nextTarget = this.nextConnection?.targetConnection ?? null; - nextTarget?.disconnect(); - if ( - previousTarget && - this.workspace.connectionChecker.canConnect( - previousTarget, - nextTarget, - false, - ) - ) { - // Attach the next statement to the previous statement. - previousTarget.connect(nextTarget!); - } + + if (!opt_healStack) return; + + // Immovable or shadow next blocks need to move along with the block; keep + // going until we encounter a normal block or run off the end of the stack. + let nextBlock = this.getNextBlock(); + while (nextBlock && (nextBlock.isShadow() || !nextBlock.isMovable())) { + nextBlock = nextBlock.getNextBlock(); + } + if (!nextBlock) return; + + // Disconnect the next statement. + const nextTarget = + nextBlock.previousConnection?.targetBlock()?.nextConnection + ?.targetConnection ?? null; + nextTarget?.disconnect(); + if ( + previousTarget && + this.workspace.connectionChecker.canConnect( + previousTarget, + nextTarget, + false, + ) + ) { + // Attach the next statement to the previous statement. + previousTarget.connect(nextTarget!); } } diff --git a/tests/mocha/block_test.js b/tests/mocha/block_test.js index 62c61ce00..e3bd47090 100644 --- a/tests/mocha/block_test.js +++ b/tests/mocha/block_test.js @@ -201,6 +201,35 @@ suite('Blocks', function () { assertUnpluggedHealFailed(blocks); }); + test('Disconnect top of stack with immovable sibling', function () { + this.blocks.B.setMovable(false); + this.blocks.A.unplug(true); + assert.equal(this.blocks.A.nextConnection.targetBlock(), this.blocks.B); + assert.isNull(this.blocks.B.nextConnection.targetBlock()); + assert.isNull(this.blocks.C.previousConnection.targetBlock()); + }); + test('Heal with immovable sibling mid-stack', function () { + const blockD = this.workspace.newBlock('stack_block', 'd'); + this.blocks.C.nextConnection.connect(blockD.previousConnection); + this.blocks.C.setMovable(false); + this.blocks.B.unplug(true); + assert.equal(this.blocks.A.nextConnection.targetBlock(), blockD); + assert.equal(this.blocks.B.nextConnection.targetBlock(), this.blocks.C); + assert.isNull(this.blocks.C.nextConnection.targetBlock()); + }); + test('Heal with immovable sibling and shadow sibling mid-stack', function () { + const blockD = this.workspace.newBlock('stack_block', 'd'); + const blockE = this.workspace.newBlock('stack_block', 'e'); + this.blocks.C.nextConnection.connect(blockD.previousConnection); + blockD.nextConnection.connect(blockE.previousConnection); + this.blocks.C.setMovable(false); + blockD.setShadow(true); + this.blocks.B.unplug(true); + assert.equal(this.blocks.A.nextConnection.targetBlock(), blockE); + assert.equal(this.blocks.B.nextConnection.targetBlock(), this.blocks.C); + assert.equal(this.blocks.C.nextConnection.targetBlock(), blockD); + assert.isNull(blockD.nextConnection.targetBlock()); + }); test('Child is shadow', function () { const blocks = this.blocks; blocks.C.setShadow(true); From 2e252a4bd80d376ab6b93524931e00d61e91683b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 09:38:45 -0700 Subject: [PATCH 30/38] chore(deps): bump google-github-actions/deploy-appengine (#9273) Bumps [google-github-actions/deploy-appengine](https://github.com/google-github-actions/deploy-appengine) from 2.1.5 to 2.1.7. - [Release notes](https://github.com/google-github-actions/deploy-appengine/releases) - [Changelog](https://github.com/google-github-actions/deploy-appengine/blob/main/CHANGELOG.md) - [Commits](https://github.com/google-github-actions/deploy-appengine/compare/v2.1.5...v2.1.7) --- updated-dependencies: - dependency-name: google-github-actions/deploy-appengine dependency-version: 2.1.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/appengine_deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/appengine_deploy.yml b/.github/workflows/appengine_deploy.yml index 1dd6d2ffa..408c1b084 100644 --- a/.github/workflows/appengine_deploy.yml +++ b/.github/workflows/appengine_deploy.yml @@ -42,7 +42,7 @@ jobs: path: _deploy/ - name: Deploy to App Engine - uses: google-github-actions/deploy-appengine@v2.1.5 + uses: google-github-actions/deploy-appengine@v2.1.7 # For parameters see: # https://github.com/google-github-actions/deploy-appengine#inputs with: From 79d314049558cd776062afcd389aef6c651f095f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 17:39:48 +0100 Subject: [PATCH 31/38] chore(deps): bump actions/download-artifact from 4 to 5 (#9287) Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 5. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/appengine_deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/appengine_deploy.yml b/.github/workflows/appengine_deploy.yml index 408c1b084..50afec240 100644 --- a/.github/workflows/appengine_deploy.yml +++ b/.github/workflows/appengine_deploy.yml @@ -36,7 +36,7 @@ jobs: needs: prepare steps: - name: Download prepared files - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: name: appengine_files path: _deploy/ From b211c02e3cac05d37126f6024afc060509b5328b Mon Sep 17 00:00:00 2001 From: RoboErikG Date: Mon, 11 Aug 2025 10:10:10 -0700 Subject: [PATCH 32/38] Change browser test timeout to 2 hours --- .github/workflows/browser_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/browser_test.yml b/.github/workflows/browser_test.yml index 3675af7b0..e670af926 100644 --- a/.github/workflows/browser_test.yml +++ b/.github/workflows/browser_test.yml @@ -11,7 +11,7 @@ permissions: jobs: build: - timeout-minutes: 10 + timeout-minutes: 120 runs-on: ${{ matrix.os }} strategy: From fb63360b9f7dc646293ea6419545ab5a8942a933 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 12 Aug 2025 08:55:54 -0700 Subject: [PATCH 33/38] refactor: Remove duplicated method from contextmenu_items.ts. (#9289) --- core/contextmenu_items.ts | 39 +++------------------------------------ 1 file changed, 3 insertions(+), 36 deletions(-) diff --git a/core/contextmenu_items.ts b/core/contextmenu_items.ts index 8bb71775f..001a3c58e 100644 --- a/core/contextmenu_items.ts +++ b/core/contextmenu_items.ts @@ -23,6 +23,7 @@ import {CommentIcon} from './icons/comment_icon.js'; import {Msg} from './msg.js'; import {StatementInput} from './renderers/zelos/zelos.js'; import {Coordinate} from './utils/coordinate.js'; +import * as svgMath from './utils/svg_math.js'; import type {WorkspaceSvg} from './workspace_svg.js'; function isFullBlockField(block?: BlockSvg) { @@ -637,9 +638,9 @@ export function registerCommentCreate() { const comment = new RenderedWorkspaceComment(workspace); comment.setPlaceholderText(Msg['WORKSPACE_COMMENT_DEFAULT_TEXT']); comment.moveTo( - pixelsToWorkspaceCoords( - new Coordinate(location.x, location.y), + svgMath.screenToWsCoordinates( workspace, + new Coordinate(location.x, location.y), ), ); getFocusManager().focusNode(comment); @@ -652,40 +653,6 @@ export function registerCommentCreate() { ContextMenuRegistry.registry.register(createOption); } -/** - * Converts pixel coordinates (relative to the window) to workspace coordinates. - */ -function pixelsToWorkspaceCoords( - pixelCoord: Coordinate, - workspace: WorkspaceSvg, -): Coordinate { - const injectionDiv = workspace.getInjectionDiv(); - // Bounding rect coordinates are in client coordinates, meaning that they - // are in pixels relative to the upper left corner of the visible browser - // window. These coordinates change when you scroll the browser window. - const boundingRect = injectionDiv.getBoundingClientRect(); - - // The client coordinates offset by the injection div's upper left corner. - const clientOffsetPixels = new Coordinate( - pixelCoord.x - boundingRect.left, - pixelCoord.y - boundingRect.top, - ); - - // The offset in pixels between the main workspace's origin and the upper - // left corner of the injection div. - const mainOffsetPixels = workspace.getOriginOffsetInPixels(); - - // The position of the new comment in pixels relative to the origin of the - // main workspace. - const finalOffset = Coordinate.difference( - clientOffsetPixels, - mainOffsetPixels, - ); - // The position of the new comment in main workspace coordinates. - finalOffset.scale(1 / workspace.scale); - return finalOffset; -} - /** Registers all block-scoped context menu items. */ function registerBlockOptions_() { registerDuplicate(); From e74910c8a0b189fe01ac79d653008be0158e6374 Mon Sep 17 00:00:00 2001 From: RoboErikG Date: Tue, 12 Aug 2025 10:32:32 -0700 Subject: [PATCH 34/38] Update block-test version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3535ae06d..d674b8637 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@blockly/block-test": "^7.0.1", + "@blockly/block-test": "^7.0.2", "@blockly/dev-tools": "^9.0.0", "@blockly/theme-modern": "^7.0.1", "@hyperjump/browser": "^1.1.4", From 4f4a450142923a9ba1f36c98836b549f556706f6 Mon Sep 17 00:00:00 2001 From: RoboErikG Date: Wed, 13 Aug 2025 09:39:24 -0700 Subject: [PATCH 35/38] Update dev-tools version in package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d674b8637..e7a496b31 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ "license": "Apache-2.0", "devDependencies": { "@blockly/block-test": "^7.0.2", - "@blockly/dev-tools": "^9.0.0", + "@blockly/dev-tools": "^9.0.2", "@blockly/theme-modern": "^7.0.1", "@hyperjump/browser": "^1.1.4", "@hyperjump/json-schema": "^1.5.0", From 7b784b58c0192cb45aa4e62ef8df072e713238f8 Mon Sep 17 00:00:00 2001 From: RoboErikG Date: Wed, 13 Aug 2025 11:06:10 -0700 Subject: [PATCH 36/38] Add a weekly schedule --- .github/workflows/browser_test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/browser_test.yml b/.github/workflows/browser_test.yml index e670af926..51ac0dffa 100644 --- a/.github/workflows/browser_test.yml +++ b/.github/workflows/browser_test.yml @@ -5,6 +5,8 @@ name: Run browser manually on: workflow_dispatch: + schedule: + - cron: '0 6 * * 1' # Runs every Monday at 06:00 UTC permissions: contents: read From 34ea176b88ce1d3f242a6930f9a58859e1d4480c Mon Sep 17 00:00:00 2001 From: Erik Pasternak Date: Wed, 13 Aug 2025 18:14:01 +0000 Subject: [PATCH 37/38] Update package-lock.json --- package-lock.json | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index b1ff8dcbd..e03f6d24d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,8 +13,8 @@ "jsdom": "26.1.0" }, "devDependencies": { - "@blockly/block-test": "^7.0.1", - "@blockly/dev-tools": "^9.0.0", + "@blockly/block-test": "^7.0.2", + "@blockly/dev-tools": "^9.0.2", "@blockly/theme-modern": "^7.0.1", "@hyperjump/browser": "^1.1.4", "@hyperjump/json-schema": "^1.5.0", @@ -90,10 +90,11 @@ "license": "ISC" }, "node_modules/@blockly/block-test": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@blockly/block-test/-/block-test-7.0.1.tgz", - "integrity": "sha512-w91ZZbpJDKGQJVO7gKqQaM17ffcsW1ktrnSTz/OpDw5R4H+1q05NgWO5gYzGPzLfFdvPcrkc0v00KhD4UG7BRA==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@blockly/block-test/-/block-test-7.0.2.tgz", + "integrity": "sha512-fwbJnMiH4EoX/CR0ZTGzSKaGfpRBn4nudquoWfvG4ekkhTjaNTldDdHvUSeyexzvwZZcT6M4I1Jtq3IoomTKEg==", "dev": true, + "license": "Apache 2.0", "engines": { "node": ">=8.17.0" }, @@ -102,13 +103,13 @@ } }, "node_modules/@blockly/dev-tools": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@blockly/dev-tools/-/dev-tools-9.0.1.tgz", - "integrity": "sha512-OnY24Up00owts0VtOaokUmOQdzH+K1PNcr3LC3huwa9PO0TlKiXTq4V5OuIqBS++enyj93gXQ8PhvFGudkogTQ==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@blockly/dev-tools/-/dev-tools-9.0.2.tgz", + "integrity": "sha512-Ic/+BkqEvLRZxzNQVW/FKXx1cB042xXXPTSmNlTv2qr4oY+hN2fwBtHj3PirBWAzWgMOF8VDTj/EXL36jH1/lg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@blockly/block-test": "^7.0.1", + "@blockly/block-test": "^7.0.2", "@blockly/theme-dark": "^8.0.1", "@blockly/theme-deuteranopia": "^7.0.1", "@blockly/theme-highcontrast": "^7.0.1", From 414f1056e8f576e86d7702ef4738f27bf9129311 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 14:26:32 +0000 Subject: [PATCH 38/38] chore(deps): bump actions/first-interaction from 2 to 3 Bumps [actions/first-interaction](https://github.com/actions/first-interaction) from 2 to 3. - [Release notes](https://github.com/actions/first-interaction/releases) - [Commits](https://github.com/actions/first-interaction/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/first-interaction dependency-version: '3' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/welcome_new_contributors.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/welcome_new_contributors.yml b/.github/workflows/welcome_new_contributors.yml index 663f03205..4c4860c25 100644 --- a/.github/workflows/welcome_new_contributors.yml +++ b/.github/workflows/welcome_new_contributors.yml @@ -9,7 +9,7 @@ jobs: permissions: pull-requests: write steps: - - uses: actions/first-interaction@v2 + - uses: actions/first-interaction@v3 with: repo-token: ${{ secrets.GITHUB_TOKEN }} pr-message: >