feat!: Invalid Blocks (#7958)

* feat: Invalid Blocks

* Rename the new json property from invalid to invalidReasons.

* Merged isValid into isEnabled.

* Minor fixes.

* More minor fixes.

* Reverting some stuff that didn't need to change.

* Addressing PR feedback.

* Update the BlockInfo interface to match State.

* Make BlockChange.disabledReason private.
This commit is contained in:
John Nesky
2024-04-17 19:47:51 -07:00
committed by GitHub
parent 7d8f88a4f1
commit cee7f916bb
26 changed files with 492 additions and 97 deletions

View File

@@ -334,6 +334,11 @@ export type ControlFlowInLoopBlock = Block & ControlFlowInLoopMixin;
interface ControlFlowInLoopMixin extends ControlFlowInLoopMixinType {}
type ControlFlowInLoopMixinType = typeof CONTROL_FLOW_IN_LOOP_CHECK_MIXIN;
/**
* The language-neutral ID for when the reason why a block is disabled is
* because the block is only valid inside of a loop.
*/
const CONTROL_FLOW_NOT_IN_LOOP_DISABLED_REASON = 'CONTROL_FLOW_NOT_IN_LOOP';
/**
* This mixin adds a check to make sure the 'controls_flow_statements' block
* is contained in a loop. Otherwise a warning is added to the block.
@@ -365,7 +370,11 @@ const CONTROL_FLOW_IN_LOOP_CHECK_MIXIN = {
// Don't change state if:
// * It's at the start of a drag.
// * It's not a move event.
if (!ws.isDragging || ws.isDragging() || e.type !== Events.BLOCK_MOVE) {
if (
!ws.isDragging ||
ws.isDragging() ||
(e.type !== Events.BLOCK_MOVE && e.type !== Events.BLOCK_CREATE)
) {
return;
}
const enabled = !!this.getSurroundLoop();
@@ -376,7 +385,10 @@ const CONTROL_FLOW_IN_LOOP_CHECK_MIXIN = {
const group = Events.getGroup();
// Makes it so the move and the disable event get undone together.
Events.setGroup(e.group);
this.setEnabled(enabled);
this.setDisabledReason(
!enabled,
CONTROL_FLOW_NOT_IN_LOOP_DISABLED_REASON,
);
Events.setGroup(group);
}
},

View File

@@ -754,7 +754,6 @@ interface CallMixin extends CallMixinType {
defType_: string;
quarkIds_: string[] | null;
quarkConnections_: {[id: string]: Connection};
previousEnabledState_: boolean;
}
type CallMixinType = typeof PROCEDURE_CALL_COMMON;
@@ -764,6 +763,13 @@ type CallExtraState = {
params?: string[];
};
/**
* The language-neutral ID for when the reason why a block is disabled is
* because the block's corresponding procedure definition is disabled.
*/
const DISABLED_PROCEDURE_DEFINITION_DISABLED_REASON =
'DISABLED_PROCEDURE_DEFINITION';
/**
* Common properties for the procedure_callnoreturn and
* procedure_callreturn blocks.
@@ -1124,12 +1130,16 @@ const PROCEDURE_CALL_COMMON = {
);
}
Events.setGroup(event.group);
if (blockChangeEvent.newValue) {
this.previousEnabledState_ = this.isEnabled();
this.setEnabled(false);
} else {
this.setEnabled(this.previousEnabledState_);
}
const valid = def.isEnabled();
this.setDisabledReason(
!valid,
DISABLED_PROCEDURE_DEFINITION_DISABLED_REASON,
);
this.setWarningText(
valid
? null
: Msg['PROCEDURES_CALL_DISABLED_DEF_WARNING'].replace('%1', name),
);
Events.setGroup(oldGroup);
}
}
@@ -1181,7 +1191,6 @@ blocks['procedures_callnoreturn'] = {
this.argumentVarModels_ = [];
this.quarkConnections_ = {};
this.quarkIds_ = null;
this.previousEnabledState_ = true;
},
defType_: 'procedures_defnoreturn',
@@ -1202,7 +1211,6 @@ blocks['procedures_callreturn'] = {
this.argumentVarModels_ = [];
this.quarkConnections_ = {};
this.quarkIds_ = null;
this.previousEnabledState_ = true;
},
defType_: 'procedures_defreturn',
@@ -1219,6 +1227,12 @@ interface IfReturnMixin extends IfReturnMixinType {
}
type IfReturnMixinType = typeof PROCEDURES_IFRETURN;
/**
* The language-neutral ID for when the reason why a block is disabled is
* because the block is only valid inside of a procedure body.
*/
const UNPARENTED_IFRETURN_DISABLED_REASON = 'UNPARENTED_IFRETURN';
const PROCEDURES_IFRETURN = {
/**
* Block for conditionally returning a value from a procedure.
@@ -1279,7 +1293,7 @@ const PROCEDURES_IFRETURN = {
if (
((this.workspace as WorkspaceSvg).isDragging &&
(this.workspace as WorkspaceSvg).isDragging()) ||
e.type !== Events.BLOCK_MOVE
(e.type !== Events.BLOCK_MOVE && e.type !== Events.BLOCK_CREATE)
) {
return; // Don't change state at the start of a drag.
}
@@ -1319,7 +1333,7 @@ const PROCEDURES_IFRETURN = {
const group = Events.getGroup();
// Makes it so the move and the disable event get undone together.
Events.setGroup(e.group);
this.setEnabled(legal);
this.setDisabledReason(!legal, UNPARENTED_IFRETURN_DISABLED_REASON);
Events.setGroup(group);
}
},

View File

@@ -25,7 +25,9 @@ import {ConnectionType} from './connection_type.js';
import * as constants from './constants.js';
import {DuplicateIconType} from './icons/exceptions.js';
import type {Abstract} from './events/events_abstract.js';
import type {BlockChange} from './events/events_block_change.js';
import type {BlockMove} from './events/events_block_move.js';
import * as deprecation from './utils/deprecation.js';
import * as eventUtils from './events/utils.js';
import * as Extensions from './extensions.js';
import type {Field} from './field.js';
@@ -166,7 +168,7 @@ export class Block implements IASTNodeLocation {
inputList: Input[] = [];
inputsInline?: boolean;
icons: IIcon[] = [];
private disabled = false;
private disabledReasons = new Set<string>();
tooltip: Tooltip.TipInfo = '';
contextMenu = true;
@@ -1390,32 +1392,89 @@ export class Block implements IASTNodeLocation {
}
/**
* Get whether this block is enabled or not.
* Get whether this block is enabled or not. A block is considered enabled
* if there aren't any reasons why it would be disabled. A block may still
* be disabled for other reasons even if the user attempts to manually
* enable it, such as when the block is in an invalid location.
*
* @returns True if enabled.
*/
isEnabled(): boolean {
return !this.disabled;
return this.disabledReasons.size === 0;
}
/** @deprecated v11 - Get whether the block is manually disabled. */
private get disabled(): boolean {
deprecation.warn(
'disabled',
'v11',
'v12',
'the isEnabled or hasDisabledReason methods of Block',
);
return this.hasDisabledReason(constants.MANUALLY_DISABLED);
}
/** @deprecated v11 - Set whether the block is manually disabled. */
private set disabled(value: boolean) {
deprecation.warn(
'disabled',
'v11',
'v12',
'the setDisabledReason method of Block',
);
this.setDisabledReason(value, constants.MANUALLY_DISABLED);
}
/**
* Set whether the block is enabled or not.
* @deprecated v11 - Set whether the block is manually enabled or disabled.
* The user can toggle whether a block is disabled from a context menu
* option. A block may still be disabled for other reasons even if the user
* attempts to manually enable it, such as when the block is in an invalid
* location. This method is deprecated and setDisabledReason should be used
* instead.
*
* @param enabled True if enabled.
*/
setEnabled(enabled: boolean) {
if (this.isEnabled() !== enabled) {
const oldValue = this.disabled;
this.disabled = !enabled;
eventUtils.fire(
new (eventUtils.get(eventUtils.BLOCK_CHANGE))(
deprecation.warn(
'setEnabled',
'v11',
'v12',
'the setDisabledReason method of Block',
);
this.setDisabledReason(!enabled, constants.MANUALLY_DISABLED);
}
/**
* Add or remove a reason why the block might be disabled. If a block has
* any reasons to be disabled, then the block itself will be considered
* disabled. A block could be disabled for multiple independent reasons
* simultaneously, such as when the user manually disables it, or the block
* is invalid.
*
* @param disabled If true, then the block should be considered disabled for
* at least the provided reason, otherwise the block is no longer disabled
* for that reason.
* @param reason A language-neutral identifier for a reason why the block
* could be disabled. Call this method again with the same identifier to
* update whether the block is currently disabled for this reason.
*/
setDisabledReason(disabled: boolean, reason: string): void {
if (this.disabledReasons.has(reason) !== disabled) {
if (disabled) {
this.disabledReasons.add(reason);
} else {
this.disabledReasons.delete(reason);
}
const blockChangeEvent = new (eventUtils.get(eventUtils.BLOCK_CHANGE))(
this,
'disabled',
null,
oldValue,
!enabled,
),
);
/* name= */ null,
/* oldValue= */ !disabled,
/* newValue= */ disabled,
) as BlockChange;
blockChangeEvent.setDisabledReason(reason);
eventUtils.fire(blockChangeEvent);
}
}
@@ -1428,7 +1487,7 @@ export class Block implements IASTNodeLocation {
getInheritedDisabled(): boolean {
let ancestor = this.getSurroundParent();
while (ancestor) {
if (ancestor.disabled) {
if (!ancestor.isEnabled()) {
return true;
}
ancestor = ancestor.getSurroundParent();
@@ -1437,6 +1496,27 @@ export class Block implements IASTNodeLocation {
return false;
}
/**
* Get whether the block is currently disabled for the provided reason.
*
* @param reason A language-neutral identifier for a reason why the block
* could be disabled.
* @returns Whether the block is disabled for the provided reason.
*/
hasDisabledReason(reason: string): boolean {
return this.disabledReasons.has(reason);
}
/**
* Get a set of reasons why the block is currently disabled, if any. If the
* block is enabled, this set will be empty.
*
* @returns The set of reasons why the block is disabled, if any.
*/
getDisabledReasons(): ReadonlySet<string> {
return this.disabledReasons;
}
/**
* Get whether the block is collapsed or not.
*

View File

@@ -29,6 +29,7 @@ import {
LegacyContextMenuOption,
} from './contextmenu_registry.js';
import type {BlockMove} from './events/events_block_move.js';
import * as deprecation from './utils/deprecation.js';
import * as eventUtils from './events/utils.js';
import type {Field} from './field.js';
import {FieldLabel} from './field_label.js';
@@ -985,17 +986,49 @@ export class BlockSvg
}
/**
* Set whether the block is enabled or not.
* @deprecated v11 - Set whether the block is manually enabled or disabled.
* The user can toggle whether a block is disabled from a context menu
* option. A block may still be disabled for other reasons even if the user
* attempts to manually enable it, such as when the block is in an invalid
* location. This method is deprecated and setDisabledReason should be used
* instead.
*
* @param enabled True if enabled.
*/
override setEnabled(enabled: boolean) {
if (this.isEnabled() !== enabled) {
deprecation.warn(
'setEnabled',
'v11',
'v12',
'the setDisabledReason method of BlockSvg',
);
const wasEnabled = this.isEnabled();
super.setEnabled(enabled);
if (!this.getInheritedDisabled()) {
if (this.isEnabled() !== wasEnabled && !this.getInheritedDisabled()) {
this.updateDisabled();
}
}
/**
* Add or remove a reason why the block might be disabled. If a block has
* any reasons to be disabled, then the block itself will be considered
* disabled. A block could be disabled for multiple independent reasons
* simultaneously, such as when the user manually disables it, or the block
* is invalid.
*
* @param disabled If true, then the block should be considered disabled for
* at least the provided reason, otherwise the block is no longer disabled
* for that reason.
* @param reason A language-neutral identifier for a reason why the block
* could be disabled. Call this method again with the same identifier to
* update whether the block is currently disabled for this reason.
*/
override setDisabledReason(disabled: boolean, reason: string): void {
const wasEnabled = this.isEnabled();
super.setDisabledReason(disabled, reason);
if (this.isEnabled() !== wasEnabled && !this.getInheritedDisabled()) {
this.updateDisabled();
}
}
/**

View File

@@ -15,3 +15,9 @@ export const COLLAPSED_INPUT_NAME = '_TEMP_COLLAPSED_INPUT';
* The language-neutral ID given to the collapsed field.
*/
export const COLLAPSED_FIELD_NAME = '_TEMP_COLLAPSED_FIELD';
/**
* The language-neutral ID for when the reason why a block is disabled is
* because the user manually disabled it, such as via the context menu.
*/
export const MANUALLY_DISABLED = 'MANUALLY_DISABLED';

View File

@@ -14,6 +14,7 @@ import {
RegistryItem,
Scope,
} from './contextmenu_registry.js';
import {MANUALLY_DISABLED} from './constants.js';
import * as dialog from './dialog.js';
import * as Events from './events/events.js';
import * as eventUtils from './events/utils.js';
@@ -458,9 +459,9 @@ export function registerCollapseExpandBlock() {
export function registerDisable() {
const disableOption: RegistryItem = {
displayText(scope: Scope) {
return scope.block!.isEnabled()
? Msg['DISABLE_BLOCK']
: Msg['ENABLE_BLOCK'];
return scope.block!.hasDisabledReason(MANUALLY_DISABLED)
? Msg['ENABLE_BLOCK']
: Msg['DISABLE_BLOCK'];
},
preconditionFn(scope: Scope) {
const block = scope.block;
@@ -469,7 +470,14 @@ export function registerDisable() {
block!.workspace.options.disable &&
block!.isEditable()
) {
if (block!.getInheritedDisabled()) {
// Determine whether this block is currently disabled for any reason
// other than the manual reason that this context menu item controls.
const disabledReasons = block!.getDisabledReasons();
const isDisabledForOtherReason =
disabledReasons.size >
(disabledReasons.has(MANUALLY_DISABLED) ? 1 : 0);
if (block!.getInheritedDisabled() || isDisabledForOtherReason) {
return 'disabled';
}
return 'enabled';
@@ -482,7 +490,10 @@ export function registerDisable() {
if (!existingGroup) {
eventUtils.setGroup(true);
}
block!.setEnabled(!block!.isEnabled());
block!.setDisabledReason(
!block!.hasDisabledReason(MANUALLY_DISABLED),
MANUALLY_DISABLED,
);
eventUtils.setGroup(existingGroup);
},
scopeType: ContextMenuRegistry.ScopeType.BLOCK,

View File

@@ -15,6 +15,7 @@ import type {Block} from '../block.js';
import type {BlockSvg} from '../block_svg.js';
import {IconType} from '../icons/icon_types.js';
import {hasBubble} from '../interfaces/i_has_bubble.js';
import {MANUALLY_DISABLED} from '../constants.js';
import * as registry from '../registry.js';
import * as utilsXml from '../utils/xml.js';
import {Workspace} from '../workspace.js';
@@ -44,6 +45,12 @@ export class BlockChange extends BlockBase {
/** The new value of the element. */
newValue: unknown;
/**
* If element is 'disabled', this is the language-neutral identifier of the
* reason why the block was or was not disabled.
*/
private disabledReason?: string;
/**
* @param opt_block The changed block. Undefined for a blank event.
* @param opt_element One of 'field', 'comment', 'disabled', etc.
@@ -86,6 +93,9 @@ export class BlockChange extends BlockBase {
json['name'] = this.name;
json['oldValue'] = this.oldValue;
json['newValue'] = this.newValue;
if (this.disabledReason) {
json['disabledReason'] = this.disabledReason;
}
return json;
}
@@ -112,9 +122,30 @@ export class BlockChange extends BlockBase {
newEvent.name = json['name'];
newEvent.oldValue = json['oldValue'];
newEvent.newValue = json['newValue'];
if (json['disabledReason'] !== undefined) {
newEvent.disabledReason = json['disabledReason'];
}
return newEvent;
}
/**
* Set the language-neutral identifier for the reason why the block was or was
* not disabled. This is only valid for events where element is 'disabled'.
* Defaults to 'MANUALLY_DISABLED'.
*
* @param disabledReason The identifier of the reason why the block was or was
* not disabled.
*/
setDisabledReason(disabledReason: string) {
if (this.element !== 'disabled') {
throw new Error(
'Cannot set the disabled reason for a BlockChange event if the ' +
'element is not "disabled".',
);
}
this.disabledReason = disabledReason;
}
/**
* Does this event record any change of state?
*
@@ -168,7 +199,10 @@ export class BlockChange extends BlockBase {
block.setCollapsed(!!value);
break;
case 'disabled':
block.setEnabled(!value);
block.setDisabledReason(
!!value,
this.disabledReason ?? MANUALLY_DISABLED,
);
break;
case 'inline':
block.setInputsInline(!!value);
@@ -219,6 +253,7 @@ export interface BlockChangeJson extends BlockBaseJson {
name?: string;
newValue: unknown;
oldValue: unknown;
disabledReason?: string;
}
registry.register(registry.Type.EVENT, eventUtils.CHANGE, BlockChange);

View File

@@ -188,6 +188,12 @@ export const COMMENT_COLLAPSE = 'comment_collapse';
*/
export const FINISHED_LOADING = 'finished_loading';
/**
* The language-neutral ID for when the reason why a block is disabled is
* because the block is not descended from a root block.
*/
const ORPHANED_BLOCK_DISABLED_REASON = 'ORPHANED_BLOCK';
/**
* Type of events that cause objects to be bumped back into the visible
* portion of the workspace.
@@ -516,10 +522,8 @@ export function get(
}
/**
* Enable/disable a block depending on whether it is properly connected.
* Set if a block is disabled depending on whether it is properly connected.
* Use this on applications where all blocks should be connected to a top block.
* Recommend setting the 'disable' option to 'false' in the config so that
* users don't try to re-enable disabled orphan blocks.
*
* @param event Custom data for event.
*/
@@ -542,17 +546,20 @@ export function disableOrphans(event: Abstract) {
try {
recordUndo = false;
const parent = block.getParent();
if (parent && parent.isEnabled()) {
if (
parent &&
!parent.hasDisabledReason(ORPHANED_BLOCK_DISABLED_REASON)
) {
const children = block.getDescendants(false);
for (let i = 0, child; (child = children[i]); i++) {
child.setEnabled(true);
child.setDisabledReason(false, ORPHANED_BLOCK_DISABLED_REASON);
}
} else if (
(block.outputConnection || block.previousConnection) &&
!eventWorkspace.isDragging()
) {
do {
block.setEnabled(false);
block.setDisabledReason(true, ORPHANED_BLOCK_DISABLED_REASON);
block = block.getNextBlock();
} while (block);
}

View File

@@ -22,6 +22,7 @@ import * as eventUtils from './events/utils.js';
import {FlyoutButton} from './flyout_button.js';
import {FlyoutMetricsManager} from './flyout_metrics_manager.js';
import type {IFlyout} from './interfaces/i_flyout.js';
import {MANUALLY_DISABLED} from './constants.js';
import type {Options} from './options.js';
import {ScrollbarPair} from './scrollbar_pair.js';
import * as blocks from './serialization/blocks.js';
@@ -43,6 +44,13 @@ enum FlyoutItemType {
BUTTON = 'button',
}
/**
* The language-neutral ID for when the reason why a block is disabled is
* because the workspace is at block capacity.
*/
const WORKSPACE_AT_BLOCK_CAPACITY_DISABLED_REASON =
'WORKSPACE_AT_BLOCK_CAPACITY';
/**
* Class for a flyout.
*/
@@ -837,6 +845,12 @@ export abstract class Flyout
blockInfo['enabled'] =
blockInfo['disabled'] !== 'true' && blockInfo['disabled'] !== true;
}
if (
blockInfo['disabledReasons'] === undefined &&
blockInfo['enabled'] === false
) {
blockInfo['disabledReasons'] = [MANUALLY_DISABLED];
}
block = blocks.appendInternal(
blockInfo as blocks.State,
this.workspace_,
@@ -1239,7 +1253,10 @@ export abstract class Flyout
common.getBlockTypeCounts(block),
);
while (block) {
block.setEnabled(enable);
block.setDisabledReason(
!enable,
WORKSPACE_AT_BLOCK_CAPACITY_DISABLED_REASON,
);
block = block.getNextBlock();
}
}

View File

@@ -9,6 +9,8 @@
import type {Block} from '../block.js';
import type {BlockSvg} from '../block_svg.js';
import type {Connection} from '../connection.js';
import {MANUALLY_DISABLED} from '../constants.js';
import * as deprecation from '../utils/deprecation.js';
import * as eventUtils from '../events/utils.js';
import {inputTypes} from '../inputs/input_types.js';
import {isSerializable} from '../interfaces/i_serializable.js';
@@ -53,6 +55,7 @@ export interface State {
movable?: boolean;
editable?: boolean;
enabled?: boolean;
disabledReasons?: string[];
inline?: boolean;
data?: string;
extraState?: AnyDuringMigration;
@@ -158,7 +161,7 @@ function saveAttributes(block: Block, state: State) {
state['collapsed'] = true;
}
if (!block.isEnabled()) {
state['enabled'] = false;
state['disabledReasons'] = Array.from(block.getDisabledReasons());
}
if (!block.isOwnDeletable()) {
state['deletable'] = false;
@@ -520,7 +523,18 @@ function loadAttributes(block: Block, state: State) {
block.setEditable(false);
}
if (state['enabled'] === false) {
block.setEnabled(false);
deprecation.warn(
'enabled',
'v11',
'v12',
'disabledReasons with the value ["' + MANUALLY_DISABLED + '"]',
);
block.setDisabledReason(true, MANUALLY_DISABLED);
}
if (Array.isArray(state['disabledReasons'])) {
for (const reason of state['disabledReasons']) {
block.setDisabledReason(true, reason);
}
}
if (state['inline'] !== undefined) {
block.setInputsInline(state['inline']);

View File

@@ -656,6 +656,7 @@ export class Trashcan
delete json['x'];
delete json['y'];
delete json['enabled'];
delete json['disabledReasons'];
if (json['icons'] && json['icons']['comment']) {
const comment = json['icons']['comment'];

View File

@@ -21,6 +21,7 @@ export interface BlockInfo {
type?: string;
gap?: string | number;
disabled?: string | boolean;
disabledReasons?: string[];
enabled?: boolean;
id?: string;
x?: number;

View File

@@ -9,6 +9,8 @@
import type {Block} from './block.js';
import type {BlockSvg} from './block_svg.js';
import type {Connection} from './connection.js';
import {MANUALLY_DISABLED} from './constants.js';
import * as deprecation from './utils/deprecation.js';
import * as eventUtils from './events/utils.js';
import type {Field} from './field.js';
import {IconType} from './icons/icon_types.js';
@@ -272,7 +274,13 @@ export function blockToDom(
element.setAttribute('collapsed', 'true');
}
if (!block.isEnabled()) {
element.setAttribute('disabled', 'true');
// Set the value of the attribute to a comma-separated list of reasons.
// Use encodeURIComponent to escape commas in the reasons so that they
// won't be confused with separator commas.
element.setAttribute(
'disabled-reasons',
Array.from(block.getDisabledReasons()).map(encodeURIComponent).join(','),
);
}
if (!block.isOwnDeletable()) {
element.setAttribute('deletable', 'false');
@@ -1015,7 +1023,24 @@ function domToBlockHeadless(
}
const disabled = xmlBlock.getAttribute('disabled');
if (disabled) {
block.setEnabled(disabled !== 'true' && disabled !== 'disabled');
deprecation.warn(
'disabled',
'v11',
'v12',
'disabled-reasons with the value "' + MANUALLY_DISABLED + '"',
);
block.setDisabledReason(
disabled === 'true' || disabled === 'disabled',
MANUALLY_DISABLED,
);
}
const disabledReasons = xmlBlock.getAttribute('disabled-reasons');
if (disabledReasons !== null) {
for (const reason of disabledReasons.split(',')) {
// Use decodeURIComponent to restore characters that were encoded in the
// value, such as commas.
block.setDisabledReason(true, decodeURIComponent(reason));
}
}
const deletable = xmlBlock.getAttribute('deletable');
if (deletable) {

View File

@@ -880,7 +880,7 @@ function fieldNameCheck(referenceBlock) {
var blocks = referenceBlock.workspace.getAllBlocks(false);
for (var i = 0, block; block = blocks[i]; i++) {
var otherName = block.getFieldValue('FIELDNAME');
if (!block.disabled && !block.getInheritedDisabled() &&
if (block.isEnabled() && !block.getInheritedDisabled() &&
otherName && otherName.toLowerCase() === name) {
count++;
}
@@ -905,7 +905,7 @@ function inputNameCheck(referenceBlock) {
var blocks = referenceBlock.workspace.getAllBlocks(false);
for (var i = 0, block; block = blocks[i]; i++) {
var otherName = block.getFieldValue('INPUTNAME');
if (!block.disabled && !block.getInheritedDisabled() &&
if (block.isEnabled() && !block.getInheritedDisabled() &&
otherName && otherName.toLowerCase() === name) {
count++;
}

View File

@@ -163,7 +163,7 @@ FactoryUtils.formatJson_ = function(blockType, rootBlock) {
var contentsBlock = rootBlock.getInputTargetBlock('INPUTS');
var lastInput = null;
while (contentsBlock) {
if (!contentsBlock.disabled && !contentsBlock.getInheritedDisabled()) {
if (contentsBlock.isEnabled() && !contentsBlock.getInheritedDisabled()) {
var fields = FactoryUtils.getFieldsJson_(
contentsBlock.getInputTargetBlock('FIELDS'));
for (var i = 0; i < fields.length; i++) {
@@ -247,7 +247,7 @@ FactoryUtils.formatJson_ = function(blockType, rootBlock) {
}
// Generate colour.
var colourBlock = rootBlock.getInputTargetBlock('COLOUR');
if (colourBlock && !colourBlock.disabled) {
if (colourBlock && colourBlock.isEnabled()) {
var hue = parseInt(colourBlock.getFieldValue('HUE'), 10);
JS.colour = hue;
}
@@ -277,7 +277,7 @@ FactoryUtils.formatJavaScript_ = function(blockType, rootBlock, workspace) {
'input_end_row': 'appendEndRowInput'};
var contentsBlock = rootBlock.getInputTargetBlock('INPUTS');
while (contentsBlock) {
if (!contentsBlock.disabled && !contentsBlock.getInheritedDisabled()) {
if (contentsBlock.isEnabled() && !contentsBlock.getInheritedDisabled()) {
var name = '';
// Dummy inputs don't have names. Other inputs do.
if (contentsBlock.type !== 'input_dummy' &&
@@ -333,7 +333,7 @@ FactoryUtils.formatJavaScript_ = function(blockType, rootBlock, workspace) {
}
// Generate colour.
var colourBlock = rootBlock.getInputTargetBlock('COLOUR');
if (colourBlock && !colourBlock.disabled) {
if (colourBlock && colourBlock.isEnabled()) {
var hue = parseInt(colourBlock.getFieldValue('HUE'), 10);
if (!isNaN(hue)) {
code.push(' this.setColour(' + hue + ');');
@@ -377,7 +377,7 @@ FactoryUtils.connectionLineJs_ = function(functionName, typeName, workspace) {
FactoryUtils.getFieldsJs_ = function(block) {
var fields = [];
while (block) {
if (!block.disabled && !block.getInheritedDisabled()) {
if (block.isEnabled() && !block.getInheritedDisabled()) {
switch (block.type) {
case 'field_static':
// Result: 'hello'
@@ -484,7 +484,7 @@ FactoryUtils.getFieldsJs_ = function(block) {
FactoryUtils.getFieldsJson_ = function(block) {
var fields = [];
while (block) {
if (!block.disabled && !block.getInheritedDisabled()) {
if (block.isEnabled() && !block.getInheritedDisabled()) {
switch (block.type) {
case 'field_static':
// Result: 'hello'
@@ -614,7 +614,7 @@ FactoryUtils.getOptTypesFrom = function(block, name) {
FactoryUtils.getTypesFrom_ = function(block, name) {
var typeBlock = block.getInputTargetBlock(name);
var types;
if (!typeBlock || typeBlock.disabled) {
if (!typeBlock || !typeBlock.isEnabled()) {
types = [];
} else if (typeBlock.type === 'type_other') {
types = [JSON.stringify(typeBlock.getFieldValue('TYPE'))];
@@ -1015,7 +1015,7 @@ FactoryUtils.savedBlockChanges = function(blockLibraryController) {
*/
FactoryUtils.getTooltipFromRootBlock_ = function(rootBlock) {
var tooltipBlock = rootBlock.getInputTargetBlock('TOOLTIP');
if (tooltipBlock && !tooltipBlock.disabled) {
if (tooltipBlock && tooltipBlock.isEnabled()) {
return tooltipBlock.getFieldValue('TEXT');
}
return '';
@@ -1029,7 +1029,7 @@ FactoryUtils.getTooltipFromRootBlock_ = function(rootBlock) {
*/
FactoryUtils.getHelpUrlFromRootBlock_ = function(rootBlock) {
var helpUrlBlock = rootBlock.getInputTargetBlock('HELPURL');
if (helpUrlBlock && !helpUrlBlock.disabled) {
if (helpUrlBlock && helpUrlBlock.isEnabled()) {
return helpUrlBlock.getFieldValue('TEXT');
}
return '';

View File

@@ -1,7 +1,7 @@
{
"@metadata": {
"author": "Ellen Spertus <ellen.spertus@gmail.com>",
"lastupdated": "2023-12-08 18:42:04.679586",
"lastupdated": "2024-03-08 22:38:32.330785",
"locale": "en",
"messagedocumentation" : "qqq"
},
@@ -369,6 +369,7 @@
"PROCEDURES_DEFNORETURN_PROCEDURE": "do something",
"PROCEDURES_BEFORE_PARAMS": "with:",
"PROCEDURES_CALL_BEFORE_PARAMS": "with:",
"PROCEDURES_CALL_DISABLED_DEF_WARNING": "Can't run the user-defined function '%1' because the definition block is disabled.",
"PROCEDURES_DEFNORETURN_DO": "",
"PROCEDURES_DEFNORETURN_TOOLTIP": "Creates a function with no output.",
"PROCEDURES_DEFNORETURN_COMMENT": "Describe this function...",

View File

@@ -375,6 +375,7 @@
"PROCEDURES_DEFNORETURN_PROCEDURE": "default name - This acts as a placeholder for the name of a function on a function definition block, as shown on [https://blockly-demo.appspot.com/static/apps/code/index.html?lang=en#w7cfju this block]. The user will replace it with the function's name.",
"PROCEDURES_BEFORE_PARAMS": "block text - This precedes the list of parameters on a function's definition block. See [https://blockly-demo.appspot.com/static/apps/code/index.html?lang=en#voztpd this sample function with parameters].",
"PROCEDURES_CALL_BEFORE_PARAMS": "block text - This precedes the list of parameters on a function's caller block. See [https://blockly-demo.appspot.com/static/apps/code/index.html?lang=en#voztpd this sample function with parameters].",
"PROCEDURES_CALL_DISABLED_DEF_WARNING": "warning - This appears if a block that runs a function can't run because the function definition block is disabled. See [https://blockly-demo.appspot.com/static/demos/code/index.html#q947d7 this sample of a disabled function definition and call block].",
"PROCEDURES_DEFNORETURN_DO": "{{Optional}}\nblock text - This appears next to the function's 'body', the blocks that should be run when the function is called, as shown in [https://blockly-demo.appspot.com/static/apps/code/index.html?lang=en#voztpd this sample function definition].",
"PROCEDURES_DEFNORETURN_TOOLTIP": "tooltip",
"PROCEDURES_DEFNORETURN_COMMENT": "Placeholder text that the user is encouraged to replace with a description of what their function does.",

View File

@@ -1489,6 +1489,12 @@ Blockly.Msg.PROCEDURES_BEFORE_PARAMS = 'with:';
/// function with parameters].
Blockly.Msg.PROCEDURES_CALL_BEFORE_PARAMS = 'with:';
/** @type {string} */
/// warning - This appears if a block that runs a function can't run because the function
/// definition block is disabled. See
/// [https://blockly-demo.appspot.com/static/demos/code/index.html#q947d7 this sample of a
/// disabled function definition and call block].
Blockly.Msg.PROCEDURES_CALL_DISABLED_DEF_WARNING = 'Can\'t run the user-defined function "%1" because the definition block is disabled.';
/** @type {string} */
/// {{Optional|Supply translation only if your language requires it. Most do not.}}
/// block text - This appears next to the function's "body", the blocks that should be
/// run when the function is called, as shown in

View File

@@ -2313,15 +2313,15 @@ suite('Blocks', function () {
.getInput('STATEMENT')
.connection.connect(blockB.previousConnection);
// Disable the block and collapse it.
blockA.setEnabled(false);
blockA.setDisabledReason(true, 'test reason');
blockA.setCollapsed(true);
// Enable the block before expanding it.
blockA.setEnabled(true);
blockA.setDisabledReason(false, 'test reason');
blockA.setCollapsed(false);
// The child blocks should be enabled.
chai.assert.isFalse(blockB.disabled);
chai.assert.isTrue(blockB.isEnabled());
chai.assert.isFalse(
blockB.getSvgRoot().classList.contains('blocklyDisabled'),
);
@@ -2334,18 +2334,18 @@ suite('Blocks', function () {
.connection.connect(blockB.previousConnection);
// Disable the child block.
blockB.setEnabled(false);
blockB.setDisabledReason(true, 'test reason');
// Collapse and disable the parent block.
blockA.setCollapsed(false);
blockA.setEnabled(false);
blockA.setDisabledReason(true, 'test reason');
// Enable the parent block.
blockA.setEnabled(true);
blockA.setDisabledReason(false, 'test reason');
blockA.setCollapsed(true);
// Child blocks should stay disabled if they have been set.
chai.assert.isTrue(blockB.disabled);
chai.assert.isFalse(blockB.isEnabled());
});
test('Disabled blocks from JSON should have proper disabled status', function () {
// Nested c-shaped blocks, inner block is disabled
@@ -2440,7 +2440,7 @@ suite('Blocks', function () {
this.child4 = this.workspace.getBlockById('child4');
});
test('Disabling parent block visually disables all descendants', async function () {
this.parent.setEnabled(false);
this.parent.setDisabledReason(true, 'test reason');
await Blockly.renderManagement.finishQueuedRenders();
for (const child of this.parent.getDescendants(false)) {
chai.assert.isTrue(
@@ -2450,9 +2450,9 @@ suite('Blocks', function () {
}
});
test('Child blocks regain original status after parent is re-enabled', async function () {
this.parent.setEnabled(false);
this.parent.setDisabledReason(true, 'test reason');
await Blockly.renderManagement.finishQueuedRenders();
this.parent.setEnabled(true);
this.parent.setDisabledReason(false, 'test reason');
await Blockly.renderManagement.finishQueuedRenders();
// child2 is disabled, rest should be enabled

View File

@@ -0,0 +1,55 @@
/**
* @license
* Copyright 2024 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import * as Blockly from '../../../build/src/core/blockly.js';
import {
sharedTestSetup,
sharedTestTeardown,
} from '../test_helpers/setup_teardown.js';
suite('Loops', function () {
setup(function () {
sharedTestSetup.call(this, {fireEventsNow: false});
this.workspace = Blockly.inject('blocklyDiv', {});
});
teardown(function () {
sharedTestTeardown.call(this);
});
suite('controls_flow_statements blocks', function () {
test('break block is invalid outside of loop block', function () {
const breakBlock = Blockly.serialization.blocks.append(
{'type': 'controls_flow_statements'},
this.workspace,
);
this.clock.runAll();
chai.assert.isFalse(
breakBlock.isEnabled(),
'Expected the break block to be disabled',
);
});
test('break block is valid inside of loop block', function () {
const loopBlock = Blockly.serialization.blocks.append(
{'type': 'controls_repeat'},
this.workspace,
);
const breakBlock = Blockly.serialization.blocks.append(
{'type': 'controls_flow_statements'},
this.workspace,
);
loopBlock
.getInput('DO')
.connection.connect(breakBlock.previousConnection);
this.clock.runAll();
chai.assert.isTrue(
breakBlock.isEnabled(),
'Expected the break block to be enabled',
);
});
});
});

View File

@@ -811,7 +811,7 @@ suite('Procedures', function () {
const defBlock = createProcDefBlock(this.workspace);
const callBlock = createProcCallBlock(this.workspace);
defBlock.setEnabled(false);
defBlock.setDisabledReason(true, 'MANUALLY_DISABLED');
this.clock.runAll();
chai.assert.isFalse(
@@ -821,16 +821,33 @@ suite('Procedures', function () {
},
);
test(
'if a procedure definition is invalid, the procedure caller ' +
'is also invalid',
function () {
const defBlock = createProcDefBlock(this.workspace);
const callBlock = createProcCallBlock(this.workspace);
defBlock.setDisabledReason(true, 'test reason');
this.clock.runAll();
chai.assert.isFalse(
callBlock.isEnabled(),
'Expected the caller block to be invalid',
);
},
);
test(
'if a procedure definition is enabled, the procedure caller ' +
'is also enabled',
function () {
const defBlock = createProcDefBlock(this.workspace);
const callBlock = createProcCallBlock(this.workspace);
defBlock.setEnabled(false);
defBlock.setDisabledReason(true, 'MANUALLY_DISABLED');
this.clock.runAll();
defBlock.setEnabled(true);
defBlock.setDisabledReason(false, 'MANUALLY_DISABLED');
this.clock.runAll();
chai.assert.isTrue(
@@ -847,12 +864,12 @@ suite('Procedures', function () {
const defBlock = createProcDefBlock(this.workspace);
const callBlock = createProcCallBlock(this.workspace);
this.clock.runAll();
callBlock.setEnabled(false);
callBlock.setDisabledReason(true, 'MANUALLY_DISABLED');
this.clock.runAll();
defBlock.setEnabled(false);
defBlock.setDisabledReason(true, 'MANUALLY_DISABLED');
this.clock.runAll();
defBlock.setEnabled(true);
defBlock.setDisabledReason(false, 'MANUALLY_DISABLED');
this.clock.runAll();
chai.assert.isFalse(
@@ -863,6 +880,36 @@ suite('Procedures', function () {
);
});
suite('procedures_ifreturn blocks', function () {
test('ifreturn block is invalid outside of def block', function () {
const ifreturnBlock = Blockly.serialization.blocks.append(
{'type': 'procedures_ifreturn'},
this.workspace,
);
this.clock.runAll();
chai.assert.isFalse(
ifreturnBlock.isEnabled(),
'Expected the ifreturn block to be invalid',
);
});
test('ifreturn block is valid inside of def block', function () {
const defBlock = createProcDefBlock(this.workspace);
const ifreturnBlock = Blockly.serialization.blocks.append(
{'type': 'procedures_ifreturn'},
this.workspace,
);
defBlock
.getInput('STACK')
.connection.connect(ifreturnBlock.previousConnection);
this.clock.runAll();
chai.assert.isTrue(
ifreturnBlock.isEnabled(),
'Expected the ifreturn block to be valid',
);
});
});
suite('deleting procedure blocks', function () {
test(
'when the procedure definition block is deleted, all of its ' +

View File

@@ -92,7 +92,7 @@ suite('Generator', function () {
return 'stack_block';
};
rowBlock.nextConnection.connect(stackBlock.previousConnection);
rowBlock.disabled = blockDisabled;
rowBlock.setDisabledReason(blockDisabled, 'test reason');
const code = generator.blockToCode(rowBlock, opt_thisOnly);
delete generator.forBlock['stack_block'];
@@ -115,11 +115,16 @@ suite('Generator', function () {
const name = testCase[1];
test(name, function () {
generator.init(this.workspace);
this.blockToCodeTest(generator, false, true, 'row_block');
this.blockToCodeTest(
generator,
false,
false,
/* blockDisabled = */ false,
/* opt_thisOnly = */ true,
'row_block',
);
this.blockToCodeTest(
generator,
/* blockDisabled = */ false,
/* opt_thisOnly = */ false,
'row_blockstack_block',
'thisOnly=false',
);
@@ -132,11 +137,16 @@ suite('Generator', function () {
const generator = testCase[0];
const name = testCase[1];
test(name, function () {
this.blockToCodeTest(generator, true, true, '');
this.blockToCodeTest(
generator,
true,
false,
/* blockDisabled = */ true,
/* opt_thisOnly = */ true,
'',
);
this.blockToCodeTest(
generator,
/* blockDisabled = */ true,
/* opt_thisOnly = */ false,
'stack_block',
'thisOnly=false',
);

View File

@@ -157,7 +157,7 @@ suite('Icon', function () {
block.addIcon(icon);
applyColourSpy.resetHistory();
block.setEnabled(false);
block.setDisabledReason(true, 'test reason');
chai.assert.isTrue(
applyColourSpy.calledOnce,
'Expected applyColour to be called',

View File

@@ -101,6 +101,7 @@
import './layering_test.js';
import './blocks/lists_test.js';
import './blocks/logic_ternary_test.js';
import './blocks/loops_test.js';
import './metrics_test.js';
import './mutator_test.js';
import './names_test.js';

View File

@@ -86,19 +86,30 @@ suite('JSO Serialization', function () {
});
});
suite('Enabled', function () {
test('False', function () {
suite('DisabledReasons', function () {
test('One reason', function () {
const block = this.workspace.newBlock('row_block');
block.setEnabled(false);
block.setDisabledReason(true, 'test reason');
const jso = Blockly.serialization.blocks.save(block);
assertProperty(jso, 'enabled', false);
assertProperty(jso, 'disabledReasons', ['test reason']);
});
test('True', function () {
test('Zero reasons', function () {
const block = this.workspace.newBlock('row_block');
block.setEnabled(true);
block.setDisabledReason(false, 'test reason');
const jso = Blockly.serialization.blocks.save(block);
assertNoProperty(jso, 'enabled');
assertNoProperty(jso, 'disabledReasons');
});
test('Multiple reasons', function () {
const block = this.workspace.newBlock('row_block');
block.setDisabledReason(true, 'test reason 1');
block.setDisabledReason(true, 'test reason 2');
const jso = Blockly.serialization.blocks.save(block);
assertProperty(jso, 'disabledReasons', [
'test reason 1',
'test reason 2',
]);
});
});

View File

@@ -81,7 +81,13 @@ Serializer.Attributes.Collapsed = new SerializerTestCase(
Serializer.Attributes.Disabled = new SerializerTestCase(
'Disabled',
'<xml xmlns="https://developers.google.com/blockly/xml">' +
'<block type="logic_negate" id="id******************" disabled="true" x="42" y="42"></block>' +
'<block type="logic_negate" id="id******************" disabled-reasons="test%20reason,another%20reason" x="42" y="42"></block>' +
'</xml>',
);
Serializer.Attributes.DisabledWithEncodedComma = new SerializerTestCase(
'DisabledWithEncodedComma',
'<xml xmlns="https://developers.google.com/blockly/xml">' +
'<block type="logic_negate" id="id******************" disabled-reasons="test%2Creason" x="42" y="42"></block>' +
'</xml>',
);
Serializer.Attributes.NotDeletable = new SerializerTestCase(
@@ -106,6 +112,7 @@ Serializer.Attributes.testCases = [
Serializer.Attributes.Basic,
Serializer.Attributes.Collapsed,
Serializer.Attributes.Disabled,
Serializer.Attributes.DisabledWithEncodedComma,
Serializer.Attributes.NotDeletable,
Serializer.Attributes.NotMovable,
Serializer.Attributes.NotEditable,