From 3f6d1ed3f6f9e88e81dffb8469f497e839398000 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 22:21:38 +0000 Subject: [PATCH 01/10] Add stacked list layout, drag end trigger and bottom sheet dialogs ListViewProperties.layout (list | stack) draws a list's items layered and placed by their bound x/y, for data shown as a diagram. TriggerType.dragEnd fires when a drag ends. ShowDialogAction.presentation shows the canvas as a dialog or a bottom sheet. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0186HDSCvPDpNcgTDiXgg3qb --- lib/src/api/models/trigger.dart | 8 +++++- lib/src/api/models/trigger.g.dart | 1 + lib/src/api/nodes/list_view_node.dart | 30 ++++++++++++++++++++ lib/src/api/nodes/list_view_node.g.dart | 10 +++++++ test/stack_drag_sheet_test.dart | 37 +++++++++++++++++++++++++ 5 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 test/stack_drag_sheet_test.dart diff --git a/lib/src/api/models/trigger.dart b/lib/src/api/models/trigger.dart index 8b75dbdd..465b8812 100644 --- a/lib/src/api/models/trigger.dart +++ b/lib/src/api/models/trigger.dart @@ -47,7 +47,11 @@ enum TriggerType { /// Triggered when the variable named by [TriggerModel.variable] changes, /// while the screen it is set on shows. - valueChanged; + valueChanged, + + /// Triggered once when the user lifts the pointer after dragging the + /// target (see [drag], which fires on every move). + dragEnd; /// Displayable string representation of the trigger type. String get prettify { @@ -78,6 +82,8 @@ enum TriggerType { return 'Load'; case TriggerType.valueChanged: return 'Value Changed'; + case TriggerType.dragEnd: + return 'Drag End'; } } } diff --git a/lib/src/api/models/trigger.g.dart b/lib/src/api/models/trigger.g.dart index d522deee..2cc1eeda 100644 --- a/lib/src/api/models/trigger.g.dart +++ b/lib/src/api/models/trigger.g.dart @@ -45,4 +45,5 @@ const _$TriggerTypeEnumMap = { TriggerType.pointerUp: 'pointerUp', TriggerType.load: 'load', TriggerType.valueChanged: 'valueChanged', + TriggerType.dragEnd: 'dragEnd', }; diff --git a/lib/src/api/nodes/list_view_node.dart b/lib/src/api/nodes/list_view_node.dart index 3dcac284..fc1c5ba7 100644 --- a/lib/src/api/nodes/list_view_node.dart +++ b/lib/src/api/nodes/list_view_node.dart @@ -164,6 +164,19 @@ enum ListItemSeparator { space, } +/// How a [ListViewNode] arranges its items. +enum ListLayout { + /// Items follow one another along the scroll direction (a normal list). + list, + + /// Items are layered on top of each other in the list's box, each placed + /// by its item's `x` and `y` (bound with `BaseNode.variables`, evaluated + /// for that item) relative to the list's padded box, or by its design + /// offset when not bound. Nothing scrolls. Use it to draw data as a + /// diagram, such as furniture on a floor plan or pins on a map. + stack, +} + /// The properties of a [ListViewNode]. @JsonSerializable() class ListViewProperties extends CustomProperties with FieldsHolder { @@ -194,8 +207,13 @@ class ListViewProperties extends CustomProperties with FieldsHolder { /// items, if enabled. DividerProperties dividerProperties; + /// How the items are arranged: one after another, or layered and placed + /// by their bound `x`/`y` (see [ListLayout.stack]). + ListLayout layout; + /// Creates a new [ListViewProperties] instance. ListViewProperties({ + this.layout = ListLayout.list, this.itemCount, this.itemExtent, this.cacheExtent, @@ -222,8 +240,10 @@ class ListViewProperties extends CustomProperties with FieldsHolder { double? dividerSpacing, bool? hasSeparator, DividerProperties? dividerProperties, + ListLayout? layout, }) { return ListViewProperties( + layout: layout ?? this.layout, itemCount: itemCount ?? this.itemCount, itemExtent: itemExtent ?? this.itemExtent, cacheExtent: cacheExtent ?? this.cacheExtent, @@ -260,6 +280,15 @@ class ListViewProperties extends CustomProperties with FieldsHolder { options: () => ListItemSeparator.values, requiresLayout: true, ), + 'layout': EnumFieldAccess( + 'Layout', + 'List: items follow one another. Stack: items are layered and placed by their bound x and y.', + () => layout, + (value) => layout = value, + defaultValue: () => ListLayout.list, + options: () => ListLayout.values, + requiresLayout: true, + ), 'dividerProperties': ObjectFieldAccess( 'Divider Properties', 'The properties of the divider to use as a separator between list view items, if enabled.', @@ -286,5 +315,6 @@ class ListViewProperties extends CustomProperties with FieldsHolder { separatorSpacing, hasSeparator, dividerProperties, + layout, ]; } diff --git a/lib/src/api/nodes/list_view_node.g.dart b/lib/src/api/nodes/list_view_node.g.dart index 2dce408d..fd680f79 100644 --- a/lib/src/api/nodes/list_view_node.g.dart +++ b/lib/src/api/nodes/list_view_node.g.dart @@ -80,6 +80,7 @@ Map _$ListViewPropertiesOmitDefaults( if (isJsonDefault(instance.separator, ListItemSeparator.space)) json.remove('separator'); if (isJsonDefault(instance.hasSeparator, false)) json.remove('hasSeparator'); + if (isJsonDefault(instance.layout, ListLayout.list)) json.remove('layout'); return json; } @@ -353,6 +354,9 @@ const _$QueryPagingEnumMap = { }; ListViewProperties _$ListViewPropertiesFromJson(Map json) => ListViewProperties( + layout: + $enumDecodeNullable(_$ListLayoutEnumMap, json['layout']) ?? + ListLayout.list, itemCount: (json['itemCount'] as num?)?.toInt(), itemExtent: (json['itemExtent'] as num?)?.toDouble(), cacheExtent: (json['cacheExtent'] as num?)?.toDouble(), @@ -375,8 +379,14 @@ Map _$ListViewPropertiesToJson(ListViewProperties instance) => 'separator': _$ListItemSeparatorEnumMap[instance.separator]!, 'hasSeparator': instance.hasSeparator, 'dividerProperties': instance.dividerProperties.toJson(), + 'layout': _$ListLayoutEnumMap[instance.layout]!, }; +const _$ListLayoutEnumMap = { + ListLayout.list: 'list', + ListLayout.stack: 'stack', +}; + const _$ListItemSeparatorEnumMap = { ListItemSeparator.divider: 'divider', ListItemSeparator.space: 'space', diff --git a/test/stack_drag_sheet_test.dart b/test/stack_drag_sheet_test.dart new file mode 100644 index 00000000..acd9e680 --- /dev/null +++ b/test/stack_drag_sheet_test.dart @@ -0,0 +1,37 @@ +import 'package:codelessly_api/codelessly_api.dart'; +import 'package:test/test.dart'; + +void main() { + test('list layout round-trips and is omitted when default', () { + final stacked = ListViewProperties(layout: ListLayout.stack); + expect(stacked.toJson()['layout'], 'stack'); + expect( + ListViewProperties.fromJson(stacked.toJson()).layout, + ListLayout.stack, + ); + expect(ListViewProperties().toJson().containsKey('layout'), isFalse); + expect(ListViewProperties.fromJson({}).layout, ListLayout.list); + }); + + test('drag end trigger round-trips', () { + final t = TriggerModel(type: TriggerType.dragEnd); + expect(TriggerModel.fromJson(t.toJson()).type, TriggerType.dragEnd); + expect(TriggerType.dragEnd.prettify, 'Drag End'); + }); + + test('dialog presentation round-trips, dialog by default', () { + final sheet = ShowDialogAction( + destinationId: 'c', + presentation: DialogPresentation.bottomSheet, + ); + expect( + ShowDialogAction.fromJson(sheet.toJson()).presentation, + DialogPresentation.bottomSheet, + ); + expect( + ShowDialogAction.fromJson(ShowDialogAction(destinationId: 'c').toJson()) + .presentation, + DialogPresentation.dialog, + ); + }); +} From 9d5ea446c2c4b77cad86ad425c10fd64585cc40a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 22:50:19 +0000 Subject: [PATCH 02/10] Spacers read without a box lock, positioning mode or reactions; drag triggers on shapes SpacerNode.fromJson required aspectRatioLock, positioningMode and reactions, so a spacer without them (nodes.insertTree) could not be created. Frames, rectangles, icons and text offer drag, drag end, pointer down and pointer up triggers in the actions panel. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0186HDSCvPDpNcgTDiXgg3qb --- lib/src/api/nodes/frame_node.dart | 4 +++ lib/src/api/nodes/icon_node.dart | 4 +++ lib/src/api/nodes/rectangle_node.dart | 4 +++ lib/src/api/nodes/spacer_node.dart | 3 +++ lib/src/api/nodes/spacer_node.g.dart | 35 +++++++++++++++++---------- lib/src/api/nodes/text_node.dart | 4 +++ 6 files changed, 41 insertions(+), 13 deletions(-) diff --git a/lib/src/api/nodes/frame_node.dart b/lib/src/api/nodes/frame_node.dart index 6bf1a6f9..ec79d080 100644 --- a/lib/src/api/nodes/frame_node.dart +++ b/lib/src/api/nodes/frame_node.dart @@ -107,6 +107,10 @@ class FrameNode extends DefaultShapeNode List get triggerTypes => [ TriggerType.click, TriggerType.longPress, + TriggerType.drag, + TriggerType.dragEnd, + TriggerType.pointerDown, + TriggerType.pointerUp, ]; /// Creates a [FrameNode] from a JSON data. diff --git a/lib/src/api/nodes/icon_node.dart b/lib/src/api/nodes/icon_node.dart index c217b72d..68f2ebc1 100644 --- a/lib/src/api/nodes/icon_node.dart +++ b/lib/src/api/nodes/icon_node.dart @@ -104,6 +104,10 @@ class IconNode extends SceneNode List get triggerTypes => [ TriggerType.click, TriggerType.longPress, + TriggerType.drag, + TriggerType.dragEnd, + TriggerType.pointerDown, + TriggerType.pointerUp, ]; /// Creates an [IconNode] from a JSON data. diff --git a/lib/src/api/nodes/rectangle_node.dart b/lib/src/api/nodes/rectangle_node.dart index 4875b198..7e1bd16c 100644 --- a/lib/src/api/nodes/rectangle_node.dart +++ b/lib/src/api/nodes/rectangle_node.dart @@ -69,6 +69,10 @@ class RectangleNode extends DefaultShapeNode with CornerMixin, ClipMixin { List get triggerTypes => [ TriggerType.click, TriggerType.longPress, + TriggerType.drag, + TriggerType.dragEnd, + TriggerType.pointerDown, + TriggerType.pointerUp, ]; @override diff --git a/lib/src/api/nodes/spacer_node.dart b/lib/src/api/nodes/spacer_node.dart index 5147c62a..cf29d178 100644 --- a/lib/src/api/nodes/spacer_node.dart +++ b/lib/src/api/nodes/spacer_node.dart @@ -41,6 +41,9 @@ class SpacerNode extends SceneNode { super.edgePins, super.parentID, super.constraints, + super.aspectRatioLock, + super.positioningMode, + super.reactions, }); /// Creates a new [SpacerNode] from a JSON data. diff --git a/lib/src/api/nodes/spacer_node.g.dart b/lib/src/api/nodes/spacer_node.g.dart index 9d7b529c..6fd5346c 100644 --- a/lib/src/api/nodes/spacer_node.g.dart +++ b/lib/src/api/nodes/spacer_node.g.dart @@ -16,11 +16,15 @@ Map _$SpacerNodeOmitDefaults( json.remove('constraints'); if (isJsonDefault(instance.edgePins, EdgePinsModel.standard)) json.remove('edgePins'); + if (isJsonDefault(instance.positioningMode, PositioningMode.align)) + json.remove('positioningMode'); if (isJsonDefault(instance.horizontalFit, SizeFit.fixed)) json.remove('horizontalFit'); if (isJsonDefault(instance.verticalFit, SizeFit.fixed)) json.remove('verticalFit'); if (isJsonDefault(instance.flex, 1)) json.remove('flex'); + if (isJsonDefault(instance.aspectRatioLock, false)) + json.remove('aspectRatioLock'); if (isJsonDefault(instance.interactionStates, {})) json.remove('interactionStates'); if (isJsonDefault(instance.tooltip, '')) json.remove('tooltip'); @@ -33,6 +37,7 @@ Map _$SpacerNodeOmitDefaults( json.remove('componentVersion'); if (isJsonDefault(instance.componentSchema, {})) json.remove('componentSchema'); + if (isJsonDefault(instance.reactions, const [])) json.remove('reactions'); return json; } @@ -62,6 +67,18 @@ SpacerNode _$SpacerNodeFromJson(Map json) => constraints: json['constraints'] == null ? const BoxConstraintsModel() : BoxConstraintsModel.fromJson(json['constraints'] as Map), + aspectRatioLock: json['aspectRatioLock'] as bool? ?? false, + positioningMode: + $enumDecodeNullable( + _$PositioningModeEnumMap, + json['positioningMode'], + ) ?? + PositioningMode.align, + reactions: + (json['reactions'] as List?) + ?.map((e) => Reaction.fromJson(e as Map)) + .toList() ?? + const [], ) ..variables = (json['variables'] as Map?)?.map( @@ -87,14 +104,6 @@ SpacerNode _$SpacerNodeFromJson(Map json) => (k, e) => MapEntry(k as String, e), ) ?? {} - ..positioningMode = $enumDecode( - _$PositioningModeEnumMap, - json['positioningMode'], - ) - ..aspectRatioLock = json['aspectRatioLock'] as bool - ..reactions = (json['reactions'] as List) - .map((e) => Reaction.fromJson(e as Map)) - .toList() ..widthFactor = (json['widthFactor'] as num?)?.toDouble() ..heightFactor = (json['heightFactor'] as num?)?.toDouble() ..interactionStates = @@ -151,16 +160,16 @@ const _$SizeFitEnumMap = { SizeFit.shrinkWrap: 'shrinkWrap', }; -const _$ComponentMarkerTypeEnumMap = { - ComponentMarkerType.component: 'component', - ComponentMarkerType.instance: 'instance', -}; - const _$PositioningModeEnumMap = { PositioningMode.align: 'align', PositioningMode.pin: 'pin', }; +const _$ComponentMarkerTypeEnumMap = { + ComponentMarkerType.component: 'component', + ComponentMarkerType.instance: 'instance', +}; + const _$InteractionStateEnumMap = { InteractionState.hovered: 'hovered', InteractionState.focused: 'focused', diff --git a/lib/src/api/nodes/text_node.dart b/lib/src/api/nodes/text_node.dart index 520bdaa1..9c0664ff 100644 --- a/lib/src/api/nodes/text_node.dart +++ b/lib/src/api/nodes/text_node.dart @@ -148,6 +148,10 @@ class TextNode extends SceneNode with BlendMixin, TextMixin, FontMixin { List get triggerTypes => [ TriggerType.click, TriggerType.longPress, + TriggerType.drag, + TriggerType.dragEnd, + TriggerType.pointerDown, + TriggerType.pointerUp, ]; @override From 89192a6d84053c2cbd08f7ab6685ffc36a889fa4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 23:15:39 +0000 Subject: [PATCH 03/10] Add popover dialog presentation Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0186HDSCvPDpNcgTDiXgg3qb --- lib/src/api/models/action/show_dialog_action.dart | 5 +++++ lib/src/api/models/action/show_dialog_action.g.dart | 1 + 2 files changed, 6 insertions(+) diff --git a/lib/src/api/models/action/show_dialog_action.dart b/lib/src/api/models/action/show_dialog_action.dart index e4cfed84..01237ac4 100644 --- a/lib/src/api/models/action/show_dialog_action.dart +++ b/lib/src/api/models/action/show_dialog_action.dart @@ -99,4 +99,9 @@ enum DialogPresentation { /// A bottom sheet on phones (narrower than 600 px), a dialog otherwise. adaptive, + + /// A menu anchored to the node that ran the action: just below it, + /// left-aligned with it (right-aligned when it would run off the screen), + /// or above it when there is no room below. Tapping outside closes it. + popover, } diff --git a/lib/src/api/models/action/show_dialog_action.g.dart b/lib/src/api/models/action/show_dialog_action.g.dart index 45efe467..0a8e1b5d 100644 --- a/lib/src/api/models/action/show_dialog_action.g.dart +++ b/lib/src/api/models/action/show_dialog_action.g.dart @@ -71,6 +71,7 @@ const _$DialogPresentationEnumMap = { DialogPresentation.dialog: 'dialog', DialogPresentation.bottomSheet: 'bottomSheet', DialogPresentation.adaptive: 'adaptive', + DialogPresentation.popover: 'popover', }; const _$ActionTypeEnumMap = { From c59612de14f63ef5f78ef7428277a30da250dbf9 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 02:46:20 +0000 Subject: [PATCH 04/10] Keep a variable's extra when saving it to a canvas withCanvas dropped extra, so a computed variable's formula was lost when the editor saved it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0186HDSCvPDpNcgTDiXgg3qb --- lib/src/api/models/variables_model.dart | 1 + test/variable_extra_test.dart | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 test/variable_extra_test.dart diff --git a/lib/src/api/models/variables_model.dart b/lib/src/api/models/variables_model.dart index 7e703dcd..4539117a 100644 --- a/lib/src/api/models/variables_model.dart +++ b/lib/src/api/models/variables_model.dart @@ -201,6 +201,7 @@ class VariableData name: name, value: value, type: type, + extra: extra, ); /// Returns the value converted to the appropriate type according to [type]. diff --git a/test/variable_extra_test.dart b/test/variable_extra_test.dart new file mode 100644 index 00000000..5819a887 --- /dev/null +++ b/test/variable_extra_test.dart @@ -0,0 +1,19 @@ +import 'package:codelessly_api/codelessly_api.dart'; +import 'package:test/test.dart'; + +void main() { + test('withCanvas keeps extra (a computed variable keeps its formula)', () { + final VariableData variable = VariableData( + id: 'shown', + name: 'shown', + type: VariableType.list, + extra: {'formula': r'${= filter(homes, h => h.published)}'}, + ); + final CanvasVariableData saved = variable.withCanvas('app'); + expect(saved.extra, variable.extra); + expect( + saved.toJson()['extra'], + {'formula': r'${= filter(homes, h => h.published)}'}, + ); + }); +} From a15a36bf7ed0b4800ae3b6389259168a9db6804d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 17:07:27 +0000 Subject: [PATCH 05/10] Key Down trigger and full height bottom sheets Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0186HDSCvPDpNcgTDiXgg3qb --- .../api/models/action/show_dialog_action.dart | 10 +++++++ .../models/action/show_dialog_action.g.dart | 3 ++ lib/src/api/models/trigger.dart | 27 +++++++++++++++-- lib/src/api/models/trigger.g.dart | 3 ++ lib/src/api/nodes/canvas_node.dart | 1 + test/key_trigger_sheet_height_test.dart | 29 +++++++++++++++++++ 6 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 test/key_trigger_sheet_height_test.dart diff --git a/lib/src/api/models/action/show_dialog_action.dart b/lib/src/api/models/action/show_dialog_action.dart index 01237ac4..507a2162 100644 --- a/lib/src/api/models/action/show_dialog_action.dart +++ b/lib/src/api/models/action/show_dialog_action.dart @@ -33,6 +33,12 @@ class ShowDialogAction extends ActionModel with Equatable, SerializableMixin { /// default. Bottom sheets always sit at the bottom. final AlignmentModel alignment; + /// For a bottom sheet: whether it is as tall as it can be (90% of the + /// screen) with the canvas filling it, so the canvas scrolls its own + /// content (a fixed header over a scrolling list). Otherwise the sheet is + /// as tall as the canvas, up to 90% of the screen, and scrolls as one. + final bool fullHeight; + /// Creates a new [ShowDialogAction]. ShowDialogAction({ required this.destinationId, @@ -42,6 +48,7 @@ class ShowDialogAction extends ActionModel with Equatable, SerializableMixin { this.showCloseButton = false, this.presentation = DialogPresentation.dialog, this.alignment = AlignmentModel.center, + this.fullHeight = false, super.nonBlocking, super.enabled, }) : params = {...params ?? {}}, @@ -56,6 +63,7 @@ class ShowDialogAction extends ActionModel with Equatable, SerializableMixin { showCloseButton, presentation, alignment, + fullHeight, ]; /// Duplicates this [ShowDialogAction] with given data overrides. @@ -67,6 +75,7 @@ class ShowDialogAction extends ActionModel with Equatable, SerializableMixin { bool? showCloseButton, DialogPresentation? presentation, AlignmentModel? alignment, + bool? fullHeight, }) => ShowDialogAction( destinationId: destinationId ?? this.destinationId, params: {...this.params, ...params ?? {}}, @@ -75,6 +84,7 @@ class ShowDialogAction extends ActionModel with Equatable, SerializableMixin { showCloseButton: showCloseButton ?? this.showCloseButton, presentation: presentation ?? this.presentation, alignment: alignment ?? this.alignment, + fullHeight: fullHeight ?? this.fullHeight, ); /// Creates a new [ShowDialogAction] instance from a JSON data. diff --git a/lib/src/api/models/action/show_dialog_action.g.dart b/lib/src/api/models/action/show_dialog_action.g.dart index 0a8e1b5d..c15399db 100644 --- a/lib/src/api/models/action/show_dialog_action.g.dart +++ b/lib/src/api/models/action/show_dialog_action.g.dart @@ -20,6 +20,7 @@ Map _$ShowDialogActionOmitDefaults( json.remove('presentation'); if (isJsonDefault(instance.alignment, AlignmentModel.center)) json.remove('alignment'); + if (isJsonDefault(instance.fullHeight, false)) json.remove('fullHeight'); if (isJsonDefault(instance.nonBlocking, false)) json.remove('nonBlocking'); if (isJsonDefault(instance.enabled, true)) json.remove('enabled'); return json; @@ -49,6 +50,7 @@ ShowDialogAction _$ShowDialogActionFromJson(Map json) => ShowDialogAction( alignment: json['alignment'] == null ? AlignmentModel.center : AlignmentModel.fromJson(json['alignment'] as Map), + fullHeight: json['fullHeight'] as bool? ?? false, nonBlocking: json['nonBlocking'] as bool? ?? false, enabled: json['enabled'] as bool? ?? true, )..type = $enumDecode(_$ActionTypeEnumMap, json['type']); @@ -65,6 +67,7 @@ Map _$ShowDialogActionToJson(ShowDialogAction instance) => 'showCloseButton': instance.showCloseButton, 'presentation': _$DialogPresentationEnumMap[instance.presentation]!, 'alignment': instance.alignment.toJson(), + 'fullHeight': instance.fullHeight, }; const _$DialogPresentationEnumMap = { diff --git a/lib/src/api/models/trigger.dart b/lib/src/api/models/trigger.dart index 465b8812..683384f8 100644 --- a/lib/src/api/models/trigger.dart +++ b/lib/src/api/models/trigger.dart @@ -51,7 +51,14 @@ enum TriggerType { /// Triggered once when the user lifts the pointer after dragging the /// target (see [drag], which fires on every move). - dragEnd; + dragEnd, + + /// Triggered when a key is pressed while the screen it is set on shows + /// and no text field has focus. The actions read `${result.key}` (the + /// key's label: `a`, `ArrowLeft`, `Delete`, `Escape`, ...), + /// `${result.shift}`, `${result.alt}` and `${result.meta}` (Command on + /// macOS, Control elsewhere). [TriggerModel.key] limits it to one key. + keyDown; /// Displayable string representation of the trigger type. String get prettify { @@ -84,6 +91,8 @@ enum TriggerType { return 'Value Changed'; case TriggerType.dragEnd: return 'Drag End'; + case TriggerType.keyDown: + return 'Key Down'; } } } @@ -103,21 +112,33 @@ class TriggerModel with Equatable, SerializableMixin { /// For [TriggerType.valueChanged], the name of the variable to watch. final String? variable; + /// For [TriggerType.keyDown], the one key it runs for (its label, as + /// `${result.key}` reads it, e.g. `ArrowLeft`), case-insensitive. Null + /// runs it for every key. + final String? key; + /// Creates a [TriggerModel] with given data. - TriggerModel({required this.type, this.debounceMs = 0, this.variable}); + TriggerModel({ + required this.type, + this.debounceMs = 0, + this.variable, + this.key, + }); @override - List get props => [type, debounceMs, variable]; + List get props => [type, debounceMs, variable, key]; /// Duplicate this [TriggerModel] with given data overrides. TriggerModel copyWith({ TriggerType? type, int? debounceMs, String? variable, + String? key, }) => TriggerModel( type: type ?? this.type, debounceMs: debounceMs ?? this.debounceMs, variable: variable ?? this.variable, + key: key ?? this.key, ); /// Factory constructor for creating a new [TriggerModel] instance from diff --git a/lib/src/api/models/trigger.g.dart b/lib/src/api/models/trigger.g.dart index 2cc1eeda..149f5653 100644 --- a/lib/src/api/models/trigger.g.dart +++ b/lib/src/api/models/trigger.g.dart @@ -22,6 +22,7 @@ TriggerModel _$TriggerModelFromJson(Map json) => TriggerModel( type: $enumDecode(_$TriggerTypeEnumMap, json['type']), debounceMs: (json['debounceMs'] as num?)?.toInt() ?? 0, variable: json['variable'] as String?, + key: json['key'] as String?, ); Map _$TriggerModelToJson(TriggerModel instance) => @@ -29,6 +30,7 @@ Map _$TriggerModelToJson(TriggerModel instance) => 'type': _$TriggerTypeEnumMap[instance.type]!, 'debounceMs': instance.debounceMs, 'variable': ?instance.variable, + 'key': ?instance.key, }; const _$TriggerTypeEnumMap = { @@ -46,4 +48,5 @@ const _$TriggerTypeEnumMap = { TriggerType.load: 'load', TriggerType.valueChanged: 'valueChanged', TriggerType.dragEnd: 'dragEnd', + TriggerType.keyDown: 'keyDown', }; diff --git a/lib/src/api/nodes/canvas_node.dart b/lib/src/api/nodes/canvas_node.dart index 9634c1f0..aa161ef3 100644 --- a/lib/src/api/nodes/canvas_node.dart +++ b/lib/src/api/nodes/canvas_node.dart @@ -46,6 +46,7 @@ class CanvasNode extends ParentNode List get triggerTypes => [ TriggerType.load, TriggerType.valueChanged, + TriggerType.keyDown, ]; @override diff --git a/test/key_trigger_sheet_height_test.dart b/test/key_trigger_sheet_height_test.dart new file mode 100644 index 00000000..7867cef6 --- /dev/null +++ b/test/key_trigger_sheet_height_test.dart @@ -0,0 +1,29 @@ +import 'package:codelessly_api/codelessly_api.dart'; +import 'package:test/test.dart'; + +void main() { + test('a key down trigger round-trips with its key', () { + final TriggerModel trigger = TriggerModel( + type: TriggerType.keyDown, + key: 'ArrowLeft', + ); + final TriggerModel read = TriggerModel.fromJson(trigger.toJson()); + expect(read.type, TriggerType.keyDown); + expect(read.key, 'ArrowLeft'); + expect(TriggerModel(type: TriggerType.keyDown).toJson()['key'], isNull); + expect(TriggerType.keyDown.prettify, 'Key Down'); + }); + + test('a full height sheet round-trips; the default is left out', () { + final ShowDialogAction sheet = ShowDialogAction( + destinationId: 'catalog', + presentation: DialogPresentation.bottomSheet, + fullHeight: true, + ); + expect(ShowDialogAction.fromJson(sheet.toJson()).fullHeight, isTrue); + expect( + ShowDialogAction(destinationId: 'catalog').toJson()['fullHeight'], + isNull, + ); + }); +} From e60d1081f54bbecb30b46903429fb05b5956fc5e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 23:47:40 +0000 Subject: [PATCH 06/10] A variable's extra counts toward its equality VariableData compared id, name, value and type only, so a change to its extra didn't notify. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0186HDSCvPDpNcgTDiXgg3qb --- lib/src/api/models/variables_model.dart | 3 ++- test/variable_extra_test.dart | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/src/api/models/variables_model.dart b/lib/src/api/models/variables_model.dart index 4539117a..e1fd9563 100644 --- a/lib/src/api/models/variables_model.dart +++ b/lib/src/api/models/variables_model.dart @@ -180,7 +180,8 @@ class VariableData } @override - List get props => [id, name, value, type]; + // Extra holds a computed variable's formula: a new formula is a change. + List get props => [id, name, value, type, extra]; @override int compareTo(VariableData other) => name.compareTo(other.name); diff --git a/test/variable_extra_test.dart b/test/variable_extra_test.dart index 5819a887..05075ccf 100644 --- a/test/variable_extra_test.dart +++ b/test/variable_extra_test.dart @@ -16,4 +16,15 @@ void main() { {'formula': r'${= filter(homes, h => h.published)}'}, ); }); + + test('a variable with a new formula is a different variable', () { + VariableData withFormula(String formula) => VariableData( + id: 'shown', + name: 'shown', + type: VariableType.list, + extra: {'formula': formula}, + ); + expect(withFormula(r'${= homes}'), withFormula(r'${= homes}')); + expect(withFormula(r'${= homes}'), isNot(withFormula(r'${= []}'))); + }); } From 47475572d73aea09be6d22bbb0c092c4d67dd60b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 00:03:13 +0000 Subject: [PATCH 07/10] Regenerate JSON code and format, as CI checks Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0186HDSCvPDpNcgTDiXgg3qb --- lib/src/api/models/variables_model.dart | 2 +- lib/src/api/nodes/text_node.g.dart | 4 ++-- test/variable_extra_test.dart | 19 ++++++++----------- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/lib/src/api/models/variables_model.dart b/lib/src/api/models/variables_model.dart index e1fd9563..bb892940 100644 --- a/lib/src/api/models/variables_model.dart +++ b/lib/src/api/models/variables_model.dart @@ -180,7 +180,7 @@ class VariableData } @override - // Extra holds a computed variable's formula: a new formula is a change. + // A change to extra (metadata the editor and apps keep) is a change. List get props => [id, name, value, type, extra]; @override diff --git a/lib/src/api/nodes/text_node.g.dart b/lib/src/api/nodes/text_node.g.dart index 01949d07..6cc3c31a 100644 --- a/lib/src/api/nodes/text_node.g.dart +++ b/lib/src/api/nodes/text_node.g.dart @@ -219,7 +219,6 @@ TextNode _$TextNodeFromJson(Map json) => ..type = json['type'] as String; Map _$TextNodeToJson(TextNode instance) => { - 'characters': instance.characters, 'variables': instance.variables, 'multipleVariables': instance.multipleVariables, 'componentId': ?instance.componentId, @@ -237,14 +236,15 @@ Map _$TextNodeToJson(TextNode instance) => { 'flex': instance.flex, 'aspectRatioLock': instance.aspectRatioLock, 'alignment': instance.alignment.toJson(), + 'characters': instance.characters, 'textMixedProps': instance.textMixedProps.map((e) => e.toJson()).toList(), 'textAlignHorizontal': _$TextAlignHorizontalEnumEnumMap[instance.textAlignHorizontal]!, 'textAlignVertical': _$TextAlignVerticalEnumEnumMap[instance.textAlignVertical]!, 'paragraphIndent': instance.paragraphIndent, - 'paragraphSpacing': instance.paragraphSpacing, 'reactions': instance.reactions.map((e) => e.toJson()).toList(), + 'paragraphSpacing': instance.paragraphSpacing, 'maxLines': ?instance.maxLines, 'overflow': _$TextOverflowCEnumMap[instance.overflow]!, 'softWrap': instance.softWrap, diff --git a/test/variable_extra_test.dart b/test/variable_extra_test.dart index 05075ccf..c5eb2b15 100644 --- a/test/variable_extra_test.dart +++ b/test/variable_extra_test.dart @@ -2,29 +2,26 @@ import 'package:codelessly_api/codelessly_api.dart'; import 'package:test/test.dart'; void main() { - test('withCanvas keeps extra (a computed variable keeps its formula)', () { + test('withCanvas keeps extra', () { final VariableData variable = VariableData( id: 'shown', name: 'shown', type: VariableType.list, - extra: {'formula': r'${= filter(homes, h => h.published)}'}, + extra: {'note': 'published homes'}, ); final CanvasVariableData saved = variable.withCanvas('app'); expect(saved.extra, variable.extra); - expect( - saved.toJson()['extra'], - {'formula': r'${= filter(homes, h => h.published)}'}, - ); + expect(saved.toJson()['extra'], {'formula': 'published homes'}); }); - test('a variable with a new formula is a different variable', () { - VariableData withFormula(String formula) => VariableData( + test('a variable with a different extra is a different variable', () { + VariableData withNote(String note) => VariableData( id: 'shown', name: 'shown', type: VariableType.list, - extra: {'formula': formula}, + extra: {'note': note}, ); - expect(withFormula(r'${= homes}'), withFormula(r'${= homes}')); - expect(withFormula(r'${= homes}'), isNot(withFormula(r'${= []}'))); + expect(withNote('a'), withNote('a')); + expect(withNote('a'), isNot(withNote('b'))); }); } From 1e006999e7558fa1e5fae4048b88bde02252a8cc Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 00:03:38 +0000 Subject: [PATCH 08/10] Test extra with a plain note, not a formula Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0186HDSCvPDpNcgTDiXgg3qb --- test/variable_extra_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/variable_extra_test.dart b/test/variable_extra_test.dart index c5eb2b15..496fcc03 100644 --- a/test/variable_extra_test.dart +++ b/test/variable_extra_test.dart @@ -11,7 +11,7 @@ void main() { ); final CanvasVariableData saved = variable.withCanvas('app'); expect(saved.extra, variable.extra); - expect(saved.toJson()['extra'], {'formula': 'published homes'}); + expect(saved.toJson()['extra'], {'note': 'published homes'}); }); test('a variable with a different extra is a different variable', () { From fb9b0fd3d1601af2f92a424a333d873723ab26dc Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 00:21:18 +0000 Subject: [PATCH 09/10] codelessly_api 6.3.0 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0186HDSCvPDpNcgTDiXgg3qb --- CHANGELOG.md | 22 ++++++++++++++++++++++ pubspec.yaml | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07d5136d..e8c1b2c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,25 @@ +## 6.3.0 + +### Layout + +- `ListViewProperties.layout`: `stack` layers a list's items in its box, + each placed at its item node's bound `x`/`y`, for diagrams drawn from + data. + +### Actions and triggers + +- `TriggerType.dragEnd` and `TriggerType.keyDown` (with `TriggerModel.key`); + drag and pointer triggers on frames, rectangles, icons and text. +- `DialogPresentation.popover`: a menu anchored to the node that ran the + action. +- `ShowDialogAction.fullHeight` for a bottom sheet whose layout scrolls its + own content. + +### Data + +- A variable's `extra` is kept by `withCanvas` and counts toward equality. +- `SpacerNode` takes a box lock, positioning mode and reactions. + ## 6.2.0 ### Layout diff --git a/pubspec.yaml b/pubspec.yaml index 4efa4b73..6f6f51e0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: codelessly_api description: Flutter's layouts and widgets represented as models and JSON data. This API is used by the Codelessly Editor and CloudUI SDK to render Flutter Widgets. -version: 6.2.0 +version: 6.3.0 homepage: https://codelessly.com/ repository: https://github.com/Codelessly/CodelesslyAPI From 73360ef3fb331edce1c5a32203a20719af78956d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 01:04:05 +0000 Subject: [PATCH 10/10] Add a Copy to Clipboard action CopyToClipboardAction copies a text (with variables and formulas), e.g. a home's share link. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0186HDSCvPDpNcgTDiXgg3qb --- CHANGELOG.md | 2 + lib/src/api/models/action/action.dart | 9 ++- .../api/models/action/api_call_action.g.dart | 1 + lib/src/api/models/action/auth_action.g.dart | 1 + .../models/action/call_function_action.g.dart | 1 + .../models/action/cloud_data_action.g.dart | 1 + .../models/action/cloud_files_action.g.dart | 1 + .../models/action/conditional_action.g.dart | 1 + .../api/models/action/confirm_action.g.dart | 1 + .../action/copy_to_clipboard_action.dart | 45 ++++++++++++++ .../action/copy_to_clipboard_action.g.dart | 62 +++++++++++++++++++ lib/src/api/models/action/delay_action.g.dart | 1 + .../api/models/action/for_each_action.g.dart | 1 + lib/src/api/models/action/link_action.g.dart | 1 + .../load_from_cloud_storage_action.g.dart | 1 + .../models/action/navigation_action.g.dart | 1 + .../action/set_cloud_storage_action.g.dart | 1 + .../models/action/set_storage_action.g.dart | 1 + .../api/models/action/set_value_action.g.dart | 1 + .../models/action/set_variable_action.g.dart | 1 + .../models/action/set_variant_action.g.dart | 1 + .../models/action/show_dialog_action.g.dart | 1 + .../models/action/show_toast_action.g.dart | 1 + .../api/models/action/submit_action.g.dart | 1 + lib/src/api/models/condition.dart | 3 + lib/src/api/models/models.dart | 1 + test/copy_to_clipboard_action_test.dart | 14 +++++ 27 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 lib/src/api/models/action/copy_to_clipboard_action.dart create mode 100644 lib/src/api/models/action/copy_to_clipboard_action.g.dart create mode 100644 test/copy_to_clipboard_action_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index e8c1b2c5..c2a48927 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ action. - `ShowDialogAction.fullHeight` for a bottom sheet whose layout scrolls its own content. +- `CopyToClipboardAction` (`ActionType.copyToClipboard`) copies a text, with + variables and formulas. ### Data diff --git a/lib/src/api/models/action/action.dart b/lib/src/api/models/action/action.dart index 117200e9..3b0c841e 100644 --- a/lib/src/api/models/action/action.dart +++ b/lib/src/api/models/action/action.dart @@ -64,7 +64,10 @@ enum ActionType { cloudData, /// Write several cloud documents together. - cloudBatch; + cloudBatch, + + /// Copy a text to the clipboard. + copyToClipboard; /// Displayable string representation of the [ActionType]. String get prettify { @@ -111,6 +114,8 @@ enum ActionType { return 'Cloud Data'; case ActionType.cloudBatch: return 'Cloud Batch'; + case ActionType.copyToClipboard: + return 'Copy to Clipboard'; } } } @@ -202,6 +207,8 @@ abstract class ActionModel with SerializableMixin { return CloudDataAction.fromJson(json); case ActionType.cloudBatch: return CloudBatchAction.fromJson(json); + case ActionType.copyToClipboard: + return CopyToClipboardAction.fromJson(json); } } diff --git a/lib/src/api/models/action/api_call_action.g.dart b/lib/src/api/models/action/api_call_action.g.dart index e84f5dd1..04e2b71b 100644 --- a/lib/src/api/models/action/api_call_action.g.dart +++ b/lib/src/api/models/action/api_call_action.g.dart @@ -60,4 +60,5 @@ const _$ActionTypeEnumMap = { ActionType.delay: 'delay', ActionType.cloudData: 'cloudData', ActionType.cloudBatch: 'cloudBatch', + ActionType.copyToClipboard: 'copyToClipboard', }; diff --git a/lib/src/api/models/action/auth_action.g.dart b/lib/src/api/models/action/auth_action.g.dart index 26e3dbd4..bcac1785 100644 --- a/lib/src/api/models/action/auth_action.g.dart +++ b/lib/src/api/models/action/auth_action.g.dart @@ -93,4 +93,5 @@ const _$ActionTypeEnumMap = { ActionType.delay: 'delay', ActionType.cloudData: 'cloudData', ActionType.cloudBatch: 'cloudBatch', + ActionType.copyToClipboard: 'copyToClipboard', }; diff --git a/lib/src/api/models/action/call_function_action.g.dart b/lib/src/api/models/action/call_function_action.g.dart index 415ad731..147ff29d 100644 --- a/lib/src/api/models/action/call_function_action.g.dart +++ b/lib/src/api/models/action/call_function_action.g.dart @@ -58,4 +58,5 @@ const _$ActionTypeEnumMap = { ActionType.delay: 'delay', ActionType.cloudData: 'cloudData', ActionType.cloudBatch: 'cloudBatch', + ActionType.copyToClipboard: 'copyToClipboard', }; diff --git a/lib/src/api/models/action/cloud_data_action.g.dart b/lib/src/api/models/action/cloud_data_action.g.dart index a6f08932..e8489b0c 100644 --- a/lib/src/api/models/action/cloud_data_action.g.dart +++ b/lib/src/api/models/action/cloud_data_action.g.dart @@ -175,6 +175,7 @@ const _$ActionTypeEnumMap = { ActionType.delay: 'delay', ActionType.cloudData: 'cloudData', ActionType.cloudBatch: 'cloudBatch', + ActionType.copyToClipboard: 'copyToClipboard', }; CloudBatchAction _$CloudBatchActionFromJson(Map json) => CloudBatchAction( diff --git a/lib/src/api/models/action/cloud_files_action.g.dart b/lib/src/api/models/action/cloud_files_action.g.dart index 8092fa15..8d75e43e 100644 --- a/lib/src/api/models/action/cloud_files_action.g.dart +++ b/lib/src/api/models/action/cloud_files_action.g.dart @@ -106,4 +106,5 @@ const _$ActionTypeEnumMap = { ActionType.delay: 'delay', ActionType.cloudData: 'cloudData', ActionType.cloudBatch: 'cloudBatch', + ActionType.copyToClipboard: 'copyToClipboard', }; diff --git a/lib/src/api/models/action/conditional_action.g.dart b/lib/src/api/models/action/conditional_action.g.dart index 48c5953c..90214fa8 100644 --- a/lib/src/api/models/action/conditional_action.g.dart +++ b/lib/src/api/models/action/conditional_action.g.dart @@ -67,4 +67,5 @@ const _$ActionTypeEnumMap = { ActionType.delay: 'delay', ActionType.cloudData: 'cloudData', ActionType.cloudBatch: 'cloudBatch', + ActionType.copyToClipboard: 'copyToClipboard', }; diff --git a/lib/src/api/models/action/confirm_action.g.dart b/lib/src/api/models/action/confirm_action.g.dart index 93096cc1..3b867e74 100644 --- a/lib/src/api/models/action/confirm_action.g.dart +++ b/lib/src/api/models/action/confirm_action.g.dart @@ -104,4 +104,5 @@ const _$ActionTypeEnumMap = { ActionType.delay: 'delay', ActionType.cloudData: 'cloudData', ActionType.cloudBatch: 'cloudBatch', + ActionType.copyToClipboard: 'copyToClipboard', }; diff --git a/lib/src/api/models/action/copy_to_clipboard_action.dart b/lib/src/api/models/action/copy_to_clipboard_action.dart new file mode 100644 index 00000000..c973030f --- /dev/null +++ b/lib/src/api/models/action/copy_to_clipboard_action.dart @@ -0,0 +1,45 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import '../../mixins.dart'; +import '../models.dart'; + +part 'copy_to_clipboard_action.g.dart'; + +/// Copies a text to the clipboard, e.g. a link to share. Pair it with a +/// toast to say so. +@JsonSerializable() +class CopyToClipboardAction extends ActionModel + with Equatable, SerializableMixin { + /// The text to copy. Supports variables and formulas, e.g. + /// `https://nook.homes/homes/${route.homeId}`. + final String text; + + /// Creates a [CopyToClipboardAction]. + CopyToClipboardAction({this.text = '', super.nonBlocking, super.enabled}) + : super(type: ActionType.copyToClipboard); + + /// Duplicates this action with the given values replaced. + CopyToClipboardAction copyWith({String? text}) => CopyToClipboardAction( + text: text ?? this.text, + nonBlocking: nonBlocking, + enabled: enabled, + ); + + @override + List get props => [text]; + + /// Creates an instance from a JSON object. + factory CopyToClipboardAction.fromJson(Map json) => + _$CopyToClipboardActionFromJson(json); + + @override + Map toJson() => _$CopyToClipboardActionOmitDefaults( + this, + _$CopyToClipboardActionToJson(this), + ); + + @override + R? accept(ActionVisitor visitor) => + visitor.visitCopyToClipboardAction(this); +} diff --git a/lib/src/api/models/action/copy_to_clipboard_action.g.dart b/lib/src/api/models/action/copy_to_clipboard_action.g.dart new file mode 100644 index 00000000..10bba825 --- /dev/null +++ b/lib/src/api/models/action/copy_to_clipboard_action.g.dart @@ -0,0 +1,62 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'copy_to_clipboard_action.dart'; + +// ************************************************************************** +// JsonDefaultsGenerator +// ************************************************************************** + +Map _$CopyToClipboardActionOmitDefaults( + CopyToClipboardAction instance, + Map json, +) { + if (isJsonDefault(instance.text, '')) json.remove('text'); + if (isJsonDefault(instance.nonBlocking, false)) json.remove('nonBlocking'); + if (isJsonDefault(instance.enabled, true)) json.remove('enabled'); + return json; +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +CopyToClipboardAction _$CopyToClipboardActionFromJson(Map json) => + CopyToClipboardAction( + text: json['text'] as String? ?? '', + nonBlocking: json['nonBlocking'] as bool? ?? false, + enabled: json['enabled'] as bool? ?? true, + )..type = $enumDecode(_$ActionTypeEnumMap, json['type']); + +Map _$CopyToClipboardActionToJson( + CopyToClipboardAction instance, +) => { + 'type': _$ActionTypeEnumMap[instance.type]!, + 'nonBlocking': instance.nonBlocking, + 'enabled': instance.enabled, + 'text': instance.text, +}; + +const _$ActionTypeEnumMap = { + ActionType.navigation: 'navigation', + ActionType.showDialog: 'showDialog', + ActionType.link: 'link', + ActionType.submit: 'submit', + ActionType.setValue: 'setValue', + ActionType.setVariant: 'setVariant', + ActionType.setVariable: 'setVariable', + ActionType.callFunction: 'callFunction', + ActionType.callApi: 'callApi', + ActionType.setStorage: 'setStorage', + ActionType.setCloudStorage: 'setCloudStorage', + ActionType.loadFromCloudStorage: 'loadFromCloudStorage', + ActionType.auth: 'auth', + ActionType.conditional: 'conditional', + ActionType.forEach: 'forEach', + ActionType.cloudFiles: 'cloudFiles', + ActionType.showToast: 'showToast', + ActionType.confirm: 'confirm', + ActionType.delay: 'delay', + ActionType.cloudData: 'cloudData', + ActionType.cloudBatch: 'cloudBatch', + ActionType.copyToClipboard: 'copyToClipboard', +}; diff --git a/lib/src/api/models/action/delay_action.g.dart b/lib/src/api/models/action/delay_action.g.dart index c7ba0fa1..2b441916 100644 --- a/lib/src/api/models/action/delay_action.g.dart +++ b/lib/src/api/models/action/delay_action.g.dart @@ -58,4 +58,5 @@ const _$ActionTypeEnumMap = { ActionType.delay: 'delay', ActionType.cloudData: 'cloudData', ActionType.cloudBatch: 'cloudBatch', + ActionType.copyToClipboard: 'copyToClipboard', }; diff --git a/lib/src/api/models/action/for_each_action.g.dart b/lib/src/api/models/action/for_each_action.g.dart index 5b5bf198..ef4071a8 100644 --- a/lib/src/api/models/action/for_each_action.g.dart +++ b/lib/src/api/models/action/for_each_action.g.dart @@ -61,4 +61,5 @@ const _$ActionTypeEnumMap = { ActionType.delay: 'delay', ActionType.cloudData: 'cloudData', ActionType.cloudBatch: 'cloudBatch', + ActionType.copyToClipboard: 'copyToClipboard', }; diff --git a/lib/src/api/models/action/link_action.g.dart b/lib/src/api/models/action/link_action.g.dart index b765bf64..c0d2cfa2 100644 --- a/lib/src/api/models/action/link_action.g.dart +++ b/lib/src/api/models/action/link_action.g.dart @@ -55,4 +55,5 @@ const _$ActionTypeEnumMap = { ActionType.delay: 'delay', ActionType.cloudData: 'cloudData', ActionType.cloudBatch: 'cloudBatch', + ActionType.copyToClipboard: 'copyToClipboard', }; diff --git a/lib/src/api/models/action/load_from_cloud_storage_action.g.dart b/lib/src/api/models/action/load_from_cloud_storage_action.g.dart index e1b753e2..c84f533f 100644 --- a/lib/src/api/models/action/load_from_cloud_storage_action.g.dart +++ b/lib/src/api/models/action/load_from_cloud_storage_action.g.dart @@ -110,6 +110,7 @@ const _$ActionTypeEnumMap = { ActionType.delay: 'delay', ActionType.cloudData: 'cloudData', ActionType.cloudBatch: 'cloudBatch', + ActionType.copyToClipboard: 'copyToClipboard', }; const _$QueryPagingEnumMap = { diff --git a/lib/src/api/models/action/navigation_action.g.dart b/lib/src/api/models/action/navigation_action.g.dart index d486f57f..569b2489 100644 --- a/lib/src/api/models/action/navigation_action.g.dart +++ b/lib/src/api/models/action/navigation_action.g.dart @@ -69,4 +69,5 @@ const _$ActionTypeEnumMap = { ActionType.delay: 'delay', ActionType.cloudData: 'cloudData', ActionType.cloudBatch: 'cloudBatch', + ActionType.copyToClipboard: 'copyToClipboard', }; diff --git a/lib/src/api/models/action/set_cloud_storage_action.g.dart b/lib/src/api/models/action/set_cloud_storage_action.g.dart index a07f4a8d..7c58f289 100644 --- a/lib/src/api/models/action/set_cloud_storage_action.g.dart +++ b/lib/src/api/models/action/set_cloud_storage_action.g.dart @@ -106,6 +106,7 @@ const _$ActionTypeEnumMap = { ActionType.delay: 'delay', ActionType.cloudData: 'cloudData', ActionType.cloudBatch: 'cloudBatch', + ActionType.copyToClipboard: 'copyToClipboard', }; AddDocumentSubAction _$AddDocumentSubActionFromJson(Map json) => diff --git a/lib/src/api/models/action/set_storage_action.g.dart b/lib/src/api/models/action/set_storage_action.g.dart index 7b461c08..92508cfe 100644 --- a/lib/src/api/models/action/set_storage_action.g.dart +++ b/lib/src/api/models/action/set_storage_action.g.dart @@ -145,4 +145,5 @@ const _$ActionTypeEnumMap = { ActionType.delay: 'delay', ActionType.cloudData: 'cloudData', ActionType.cloudBatch: 'cloudBatch', + ActionType.copyToClipboard: 'copyToClipboard', }; diff --git a/lib/src/api/models/action/set_value_action.g.dart b/lib/src/api/models/action/set_value_action.g.dart index 80eb0596..ad4a4471 100644 --- a/lib/src/api/models/action/set_value_action.g.dart +++ b/lib/src/api/models/action/set_value_action.g.dart @@ -121,6 +121,7 @@ const _$ActionTypeEnumMap = { ActionType.delay: 'delay', ActionType.cloudData: 'cloudData', ActionType.cloudBatch: 'cloudBatch', + ActionType.copyToClipboard: 'copyToClipboard', }; BoolValue _$BoolValueFromJson(Map json) => BoolValue( diff --git a/lib/src/api/models/action/set_variable_action.g.dart b/lib/src/api/models/action/set_variable_action.g.dart index 6c872e2b..c5f9df64 100644 --- a/lib/src/api/models/action/set_variable_action.g.dart +++ b/lib/src/api/models/action/set_variable_action.g.dart @@ -112,4 +112,5 @@ const _$ActionTypeEnumMap = { ActionType.delay: 'delay', ActionType.cloudData: 'cloudData', ActionType.cloudBatch: 'cloudBatch', + ActionType.copyToClipboard: 'copyToClipboard', }; diff --git a/lib/src/api/models/action/set_variant_action.g.dart b/lib/src/api/models/action/set_variant_action.g.dart index 1fe3fd9e..2aab7b75 100644 --- a/lib/src/api/models/action/set_variant_action.g.dart +++ b/lib/src/api/models/action/set_variant_action.g.dart @@ -57,4 +57,5 @@ const _$ActionTypeEnumMap = { ActionType.delay: 'delay', ActionType.cloudData: 'cloudData', ActionType.cloudBatch: 'cloudBatch', + ActionType.copyToClipboard: 'copyToClipboard', }; diff --git a/lib/src/api/models/action/show_dialog_action.g.dart b/lib/src/api/models/action/show_dialog_action.g.dart index c15399db..48e7314e 100644 --- a/lib/src/api/models/action/show_dialog_action.g.dart +++ b/lib/src/api/models/action/show_dialog_action.g.dart @@ -99,4 +99,5 @@ const _$ActionTypeEnumMap = { ActionType.delay: 'delay', ActionType.cloudData: 'cloudData', ActionType.cloudBatch: 'cloudBatch', + ActionType.copyToClipboard: 'copyToClipboard', }; diff --git a/lib/src/api/models/action/show_toast_action.g.dart b/lib/src/api/models/action/show_toast_action.g.dart index 684de2c2..1a114022 100644 --- a/lib/src/api/models/action/show_toast_action.g.dart +++ b/lib/src/api/models/action/show_toast_action.g.dart @@ -113,4 +113,5 @@ const _$ActionTypeEnumMap = { ActionType.delay: 'delay', ActionType.cloudData: 'cloudData', ActionType.cloudBatch: 'cloudBatch', + ActionType.copyToClipboard: 'copyToClipboard', }; diff --git a/lib/src/api/models/action/submit_action.g.dart b/lib/src/api/models/action/submit_action.g.dart index bca44fb0..7e442af1 100644 --- a/lib/src/api/models/action/submit_action.g.dart +++ b/lib/src/api/models/action/submit_action.g.dart @@ -73,6 +73,7 @@ const _$ActionTypeEnumMap = { ActionType.delay: 'delay', ActionType.cloudData: 'cloudData', ActionType.cloudBatch: 'cloudBatch', + ActionType.copyToClipboard: 'copyToClipboard', }; MailchimpSubmitAction _$MailchimpSubmitActionFromJson(Map json) => diff --git a/lib/src/api/models/condition.dart b/lib/src/api/models/condition.dart index 1af898e7..40511bf8 100644 --- a/lib/src/api/models/condition.dart +++ b/lib/src/api/models/condition.dart @@ -782,6 +782,9 @@ abstract interface class ActionVisitor { /// Evaluates the action and returns the result. R? visitDelayAction(DelayAction action); + /// Visits a [CopyToClipboardAction]. + R? visitCopyToClipboardAction(CopyToClipboardAction action); + /// Visits a [CloudDataAction]. R? visitCloudDataAction(CloudDataAction action); diff --git a/lib/src/api/models/models.dart b/lib/src/api/models/models.dart index 3b86dfac..bac50bff 100644 --- a/lib/src/api/models/models.dart +++ b/lib/src/api/models/models.dart @@ -4,6 +4,7 @@ export 'action/api_call_action.dart'; export 'action/auth_action.dart'; export 'action/cloud_files_action.dart'; export 'action/conditional_action.dart'; +export 'action/copy_to_clipboard_action.dart'; export 'action/for_each_action.dart'; export 'action/call_function_action.dart'; export 'action/link_action.dart'; diff --git a/test/copy_to_clipboard_action_test.dart b/test/copy_to_clipboard_action_test.dart new file mode 100644 index 00000000..0d7739e5 --- /dev/null +++ b/test/copy_to_clipboard_action_test.dart @@ -0,0 +1,14 @@ +import 'package:codelessly_api/codelessly_api.dart'; +import 'package:test/test.dart'; + +void main() { + test('a copy to clipboard action round-trips through JSON', () { + final CopyToClipboardAction action = CopyToClipboardAction( + text: r'https://nook.homes/homes/${route.homeId}', + ); + final ActionModel read = ActionModel.fromJson(action.toJson()); + expect(read, isA()); + expect((read as CopyToClipboardAction).text, action.text); + expect(read.type, ActionType.copyToClipboard); + }); +}