From 1e1cf622b3dccf93c323010b52a4b9c05d932c0d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 02:04:34 +0000 Subject: [PATCH] Placeholders read JSON without empty allowedTypes/deniedTypes Layouts published before the merge with master left empty type lists out of placeholder JSON; master's reader required them, so republishing a project failed reading its published layouts (variance_node.g.dart: type 'Null' is not a subtype of type 'List'). Read a missing list as empty; writing and per-node defaults are unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013oMokUGNW6txknJs7xxTik --- CHANGELOG.md | 3 ++ lib/src/api/nodes/grid_view_node.g.dart | 4 +- lib/src/api/nodes/list_view_node.g.dart | 4 +- lib/src/api/nodes/page_view_node.g.dart | 4 +- .../api/nodes/single_placeholder_node.dart | 6 +++ .../api/nodes/single_placeholder_node.g.dart | 4 +- lib/src/api/nodes/variance_node.g.dart | 4 +- test/placeholder_types_missing_test.dart | 37 +++++++++++++++++++ 8 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 test/placeholder_types_missing_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 42bd43f0..dfe7a767 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -108,6 +108,9 @@ ### Fixes +- Placeholders (variances, lists, grids, pages) read JSON that leaves out + empty `allowedTypes` or `deniedTypes`, as JSON written before this + release could. - `SpacerNode.fromJson` defaults `positioningMode` (align), `aspectRatioLock` (false) and `reactions` ([]) like every other node, and `GridViewNode.fromJson` defaults `isScrollable` (true) and diff --git a/lib/src/api/nodes/grid_view_node.g.dart b/lib/src/api/nodes/grid_view_node.g.dart index 5febad68..ab43ee6d 100644 --- a/lib/src/api/nodes/grid_view_node.g.dart +++ b/lib/src/api/nodes/grid_view_node.g.dart @@ -259,14 +259,14 @@ GridViewNode _$GridViewNodeFromJson(Map json) => ..motion = json['motion'] == null ? null : MotionModel.fromJson(json['motion'] as Map) - ..deniedTypes = (json['deniedTypes'] as List) + ..deniedTypes = (readTypesOrEmpty(json, 'deniedTypes') as List) .map((e) => e as String) .toList() ..maxAllowedSize = json['maxAllowedSize'] == null ? null : SizeC.fromJson(json['maxAllowedSize'] as Map) ..ephemeral = json['ephemeral'] as bool? ?? false - ..allowedTypes = (json['allowedTypes'] as List) + ..allowedTypes = (readTypesOrEmpty(json, 'allowedTypes') as List) .map((e) => e as String) .toList() ..paging = diff --git a/lib/src/api/nodes/list_view_node.g.dart b/lib/src/api/nodes/list_view_node.g.dart index bb25f584..992d1210 100644 --- a/lib/src/api/nodes/list_view_node.g.dart +++ b/lib/src/api/nodes/list_view_node.g.dart @@ -235,14 +235,14 @@ ListViewNode _$ListViewNodeFromJson(Map json) => ..motion = json['motion'] == null ? null : MotionModel.fromJson(json['motion'] as Map) - ..deniedTypes = (json['deniedTypes'] as List) + ..deniedTypes = (readTypesOrEmpty(json, 'deniedTypes') as List) .map((e) => e as String) .toList() ..maxAllowedSize = json['maxAllowedSize'] == null ? null : SizeC.fromJson(json['maxAllowedSize'] as Map) ..ephemeral = json['ephemeral'] as bool? ?? false - ..allowedTypes = (json['allowedTypes'] as List) + ..allowedTypes = (readTypesOrEmpty(json, 'allowedTypes') as List) .map((e) => e as String) .toList() ..isScrollable = json['isScrollable'] as bool diff --git a/lib/src/api/nodes/page_view_node.g.dart b/lib/src/api/nodes/page_view_node.g.dart index 541dfd32..f93ae6cb 100644 --- a/lib/src/api/nodes/page_view_node.g.dart +++ b/lib/src/api/nodes/page_view_node.g.dart @@ -204,10 +204,10 @@ PageViewNode _$PageViewNodeFromJson(Map json) => ? null : MotionModel.fromJson(json['motion'] as Map) ..ephemeral = json['ephemeral'] as bool? ?? false - ..allowedTypes = (json['allowedTypes'] as List) + ..allowedTypes = (readTypesOrEmpty(json, 'allowedTypes') as List) .map((e) => e as String) .toList() - ..deniedTypes = (json['deniedTypes'] as List) + ..deniedTypes = (readTypesOrEmpty(json, 'deniedTypes') as List) .map((e) => e as String) .toList() ..maxAllowedSize = json['maxAllowedSize'] == null diff --git a/lib/src/api/nodes/single_placeholder_node.dart b/lib/src/api/nodes/single_placeholder_node.dart index 2cfa6ca2..6bf5668a 100644 --- a/lib/src/api/nodes/single_placeholder_node.dart +++ b/lib/src/api/nodes/single_placeholder_node.dart @@ -34,12 +34,14 @@ class SinglePlaceholderNode extends SceneNode /// The node types that are allowed to be inside this [SinglePlaceholderNode]. /// Example: 'APP_BAR' for AppBarNode. /// When empty, allow all types. + @JsonKey(readValue: readTypesOrEmpty) List allowedTypes; /// The node types that are not allowed to be inside this /// [SinglePlaceholderNode]. /// Example: "TEXT" for TextNode. /// When empty, allow all types. + @JsonKey(readValue: readTypesOrEmpty) List deniedTypes; /// If not null, this [SinglePlaceholderNode] will only accept nodes that do @@ -89,3 +91,7 @@ class SinglePlaceholderNode extends SceneNode _$SinglePlaceholderNodeToJson(this), ); } + +/// Reads [allowedTypes] or [deniedTypes] from [json], an empty list when the +/// key is missing: JSON that leaves empty lists out still reads. +Object? readTypesOrEmpty(Map json, String key) => json[key] ?? const []; diff --git a/lib/src/api/nodes/single_placeholder_node.g.dart b/lib/src/api/nodes/single_placeholder_node.g.dart index 72d475b8..c3b2f15f 100644 --- a/lib/src/api/nodes/single_placeholder_node.g.dart +++ b/lib/src/api/nodes/single_placeholder_node.g.dart @@ -57,10 +57,10 @@ Map _$SinglePlaceholderNodeOmitDefaults( SinglePlaceholderNode _$SinglePlaceholderNodeFromJson(Map json) => SinglePlaceholderNode( - allowedTypes: (json['allowedTypes'] as List) + allowedTypes: (readTypesOrEmpty(json, 'allowedTypes') as List) .map((e) => e as String) .toList(), - deniedTypes: (json['deniedTypes'] as List) + deniedTypes: (readTypesOrEmpty(json, 'deniedTypes') as List) .map((e) => e as String) .toList(), maxAllowedSize: json['maxAllowedSize'] == null diff --git a/lib/src/api/nodes/variance_node.g.dart b/lib/src/api/nodes/variance_node.g.dart index fcfd033f..6f38375a 100644 --- a/lib/src/api/nodes/variance_node.g.dart +++ b/lib/src/api/nodes/variance_node.g.dart @@ -177,10 +177,10 @@ VarianceNode _$VarianceNodeFromJson(Map json) => ? null : MotionModel.fromJson(json['motion'] as Map) ..ephemeral = json['ephemeral'] as bool? ?? false - ..allowedTypes = (json['allowedTypes'] as List) + ..allowedTypes = (readTypesOrEmpty(json, 'allowedTypes') as List) .map((e) => e as String) .toList() - ..deniedTypes = (json['deniedTypes'] as List) + ..deniedTypes = (readTypesOrEmpty(json, 'deniedTypes') as List) .map((e) => e as String) .toList() ..type = json['type'] as String; diff --git a/test/placeholder_types_missing_test.dart b/test/placeholder_types_missing_test.dart new file mode 100644 index 00000000..706697af --- /dev/null +++ b/test/placeholder_types_missing_test.dart @@ -0,0 +1,37 @@ +import 'package:codelessly_api/codelessly_api.dart'; +import 'package:test/test.dart'; + +void main() { + test('placeholders read JSON that leaves out empty type lists', () { + for (final String type in ['variance', 'listView', 'gridView']) { + final Map json = { + 'type': type, + 'id': 'x', + 'name': 'n', + 'basicBoxLocal': [0, 0, 100, 100], + 'children': [], + 'properties': {}, + if (type == 'variance') ...{ + 'variants': [ + {'id': 'default', 'name': 'Default', 'children': []}, + ], + 'currentVariantId': 'default', + }, + if (type == 'gridView') + 'properties': { + 'gridDelegate': { + 'type': 'fixedCrossAxisCount', + 'crossAxisCount': 2, + }, + }, + }; + json.remove('allowedTypes'); + json.remove('deniedTypes'); + final node = NodeJsonConverter().fromJson(json) as SinglePlaceholderNode; + expect(node.allowedTypes, isEmpty, reason: type); + // A list keeps its own default; a variance reads it as empty. + if (type == 'variance') expect(node.deniedTypes, isEmpty); + if (type == 'listView') expect(node.deniedTypes, ['listView']); + } + }); +}