diff --git a/CHANGELOG.md b/CHANGELOG.md index 0884197..f2d4318 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,25 @@ +## 5.2.0 + +### New actions + +- `AuthAction` (`ActionType.auth`): sign up, sign in, sign out, update the + profile and send a password reset email (`AuthOperation`), with an + optional error variable. +- `ConditionalAction` (`ActionType.conditional`): an expression with + then/else action lists, so action chains can branch. +- `ForEachAction` (`ActionType.forEach`): runs actions for each item of a + list variable. +- `CloudFilesAction` (`ActionType.cloudFiles`): picks and uploads images to + the project's storage, or deletes a file, running nested actions per file. + +### Changes + +- `RowColumnNode` has a corner radius. +- 8-digit `#AARRGGBB` colors set the paint's opacity when none is given. +- Dates in list and map variables encode as ISO 8601 strings + (`encodeJsonValue`), and `VariableType.fromObjectType` maps `DateTime` to + text. + ## 5.1.0 ### New nodes diff --git a/lib/src/api/models/action/action.dart b/lib/src/api/models/action/action.dart index 0f99f2c..86932f5 100644 --- a/lib/src/api/models/action/action.dart +++ b/lib/src/api/models/action/action.dart @@ -37,7 +37,19 @@ enum ActionType { setCloudStorage, /// Load data from cloud storage. - loadFromCloudStorage; + loadFromCloudStorage, + + /// Sign app users in and out. + auth, + + /// Branch on an expression. + conditional, + + /// Run actions for each item of a list. + forEach, + + /// Upload or delete files in cloud storage. + cloudFiles; /// Displayable string representation of the [ActionType]. String get prettify { @@ -66,6 +78,14 @@ enum ActionType { return 'Set Cloud Storage'; case ActionType.loadFromCloudStorage: return 'Load from Cloud Storage'; + case ActionType.auth: + return 'Authentication'; + case ActionType.conditional: + return 'If / Else'; + case ActionType.forEach: + return 'For Each'; + case ActionType.cloudFiles: + return 'Cloud Files'; } } } @@ -139,6 +159,14 @@ abstract class ActionModel with SerializableMixin { return SetCloudStorageAction.fromJson(json); case ActionType.loadFromCloudStorage: return LoadFromCloudStorageAction.fromJson(json); + case ActionType.auth: + return AuthAction.fromJson(json); + case ActionType.conditional: + return ConditionalAction.fromJson(json); + case ActionType.forEach: + return ForEachAction.fromJson(json); + case ActionType.cloudFiles: + return CloudFilesAction.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 88e2c3a..b6e55a1 100644 --- a/lib/src/api/models/action/api_call_action.g.dart +++ b/lib/src/api/models/action/api_call_action.g.dart @@ -50,4 +50,8 @@ const _$ActionTypeEnumMap = { ActionType.setStorage: 'setStorage', ActionType.setCloudStorage: 'setCloudStorage', ActionType.loadFromCloudStorage: 'loadFromCloudStorage', + ActionType.auth: 'auth', + ActionType.conditional: 'conditional', + ActionType.forEach: 'forEach', + ActionType.cloudFiles: 'cloudFiles', }; diff --git a/lib/src/api/models/action/auth_action.dart b/lib/src/api/models/action/auth_action.dart new file mode 100644 index 0000000..288274d --- /dev/null +++ b/lib/src/api/models/action/auth_action.dart @@ -0,0 +1,112 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import '../../mixins.dart'; +import '../models.dart'; + +part 'auth_action.g.dart'; + +/// What an [AuthAction] does to the app's signed-in user. +enum AuthOperation { + /// Signs in with [AuthAction.email] and [AuthAction.password]. + signIn, + + /// Creates an account with [AuthAction.email] and [AuthAction.password], + /// and sets its display name to [AuthAction.displayName] when given. + signUp, + + /// Signs the user out. + signOut, + + /// Sets the signed-in user's display name to [AuthAction.displayName]. + updateProfile, + + /// Sends a password reset email to [AuthAction.email]. + sendPasswordResetEmail; + + /// A readable name for this operation. + String get label => switch (this) { + signIn => 'Sign In', + signUp => 'Sign Up', + signOut => 'Sign Out', + updateProfile => 'Update Profile', + sendPasswordResetEmail => 'Send Password Reset Email', + }; +} + +/// Signs app users in and out with email and password. +/// +/// The signed-in user is available to layouts as the `auth` variable: +/// `${auth.uid}`, `${auth.email}`, `${auth.displayName}` and +/// `${auth.isSignedIn}`. +/// +/// [email], [password] and [displayName] may contain variable paths, e.g. +/// `${email}`. When the operation fails, the action fails, which stops the +/// actions after it, and a readable message is written to [errorVariable] +/// when one is set. On success [errorVariable] is cleared. +@JsonSerializable() +class AuthAction extends ActionModel with Equatable, SerializableMixin { + /// What to do. + final AuthOperation operation; + + /// The email address, for [AuthOperation.signIn], [AuthOperation.signUp] + /// and [AuthOperation.sendPasswordResetEmail]. + final String email; + + /// The password, for [AuthOperation.signIn] and [AuthOperation.signUp]. + final String password; + + /// The display name, for [AuthOperation.signUp] and + /// [AuthOperation.updateProfile]. + final String displayName; + + /// A text variable that receives the error message when the operation + /// fails, and is cleared when it succeeds. + final VariableData? errorVariable; + + /// Creates an [AuthAction]. + AuthAction({ + this.operation = AuthOperation.signIn, + this.email = '', + this.password = '', + this.displayName = '', + this.errorVariable, + super.nonBlocking, + super.enabled, + }) : super(type: ActionType.auth); + + /// Duplicates this action with the given values replaced. + AuthAction copyWith({ + AuthOperation? operation, + String? email, + String? password, + String? displayName, + VariableData? errorVariable, + }) => AuthAction( + operation: operation ?? this.operation, + email: email ?? this.email, + password: password ?? this.password, + displayName: displayName ?? this.displayName, + errorVariable: errorVariable ?? this.errorVariable, + nonBlocking: nonBlocking, + enabled: enabled, + ); + + @override + List get props => [ + operation, + email, + password, + displayName, + errorVariable, + ]; + + /// Creates an instance from a JSON object. + factory AuthAction.fromJson(Map json) => _$AuthActionFromJson(json); + + @override + Map toJson() => _$AuthActionOmitDefaults(this, _$AuthActionToJson(this)); + + @override + R? accept(ActionVisitor visitor) => visitor.visitAuthAction(this); +} diff --git a/lib/src/api/models/action/auth_action.g.dart b/lib/src/api/models/action/auth_action.g.dart new file mode 100644 index 0000000..cf15e6c --- /dev/null +++ b/lib/src/api/models/action/auth_action.g.dart @@ -0,0 +1,80 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'auth_action.dart'; + +// ************************************************************************** +// JsonDefaultsGenerator +// ************************************************************************** + +Map _$AuthActionOmitDefaults( + AuthAction instance, + Map json, +) { + if (isJsonDefault(instance.operation, AuthOperation.signIn)) + json.remove('operation'); + if (isJsonDefault(instance.email, '')) json.remove('email'); + if (isJsonDefault(instance.password, '')) json.remove('password'); + if (isJsonDefault(instance.displayName, '')) json.remove('displayName'); + if (isJsonDefault(instance.nonBlocking, false)) json.remove('nonBlocking'); + if (isJsonDefault(instance.enabled, true)) json.remove('enabled'); + return json; +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AuthAction _$AuthActionFromJson(Map json) => AuthAction( + operation: + $enumDecodeNullable(_$AuthOperationEnumMap, json['operation']) ?? + AuthOperation.signIn, + email: json['email'] as String? ?? '', + password: json['password'] as String? ?? '', + displayName: json['displayName'] as String? ?? '', + errorVariable: json['errorVariable'] == null + ? null + : VariableData.fromJson( + Map.from(json['errorVariable'] as Map), + ), + nonBlocking: json['nonBlocking'] as bool? ?? false, + enabled: json['enabled'] as bool? ?? true, +)..type = $enumDecode(_$ActionTypeEnumMap, json['type']); + +Map _$AuthActionToJson(AuthAction instance) => + { + 'type': _$ActionTypeEnumMap[instance.type]!, + 'nonBlocking': instance.nonBlocking, + 'enabled': instance.enabled, + 'operation': _$AuthOperationEnumMap[instance.operation]!, + 'email': instance.email, + 'password': instance.password, + 'displayName': instance.displayName, + 'errorVariable': ?instance.errorVariable?.toJson(), + }; + +const _$AuthOperationEnumMap = { + AuthOperation.signIn: 'signIn', + AuthOperation.signUp: 'signUp', + AuthOperation.signOut: 'signOut', + AuthOperation.updateProfile: 'updateProfile', + AuthOperation.sendPasswordResetEmail: 'sendPasswordResetEmail', +}; + +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', +}; 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 243af1f..dab2853 100644 --- a/lib/src/api/models/action/call_function_action.g.dart +++ b/lib/src/api/models/action/call_function_action.g.dart @@ -48,4 +48,8 @@ const _$ActionTypeEnumMap = { ActionType.setStorage: 'setStorage', ActionType.setCloudStorage: 'setCloudStorage', ActionType.loadFromCloudStorage: 'loadFromCloudStorage', + ActionType.auth: 'auth', + ActionType.conditional: 'conditional', + ActionType.forEach: 'forEach', + ActionType.cloudFiles: 'cloudFiles', }; diff --git a/lib/src/api/models/action/cloud_files_action.dart b/lib/src/api/models/action/cloud_files_action.dart new file mode 100644 index 0000000..943a663 --- /dev/null +++ b/lib/src/api/models/action/cloud_files_action.dart @@ -0,0 +1,128 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import '../../mixins.dart'; +import '../models.dart'; + +part 'cloud_files_action.g.dart'; + +/// What a [CloudFilesAction] does. +enum CloudFilesOperation { + /// Lets the user pick files and uploads them. + upload, + + /// Deletes the file at [CloudFilesAction.path]. + delete; + + /// A readable name for this operation. + String get label => switch (this) { + upload => 'Upload Files', + delete => 'Delete File', + }; +} + +/// Which files the picker offers for [CloudFilesOperation.upload]. +enum CloudFilesType { + /// Images only. + image, + + /// Any file. + any, +} + +/// Uploads files the user picks to the project's cloud storage, or deletes +/// one. +/// +/// Files are stored under the project's own folder, so [path] is relative to +/// it. For an upload, [path] is the folder (e.g. `albums/${route.albumId}`) +/// and each file is stored there under a new id with its extension. For a +/// delete, [path] is the file's path as an upload reported it. +/// +/// After each file is uploaded, [actions] run with `${result}` set to the +/// file: `${result.url}` (download URL), `${result.path}`, `${result.id}`, +/// `${result.name}`, `${result.extension}`, `${result.contentType}`, +/// `${result.index}` and `${result.total}`. +/// +/// [progressVariable], when set, is a map variable that receives +/// `{"uploading": bool, "current": int, "total": int}` while files upload. +@JsonSerializable() +class CloudFilesAction extends ActionModel with Equatable, SerializableMixin { + /// What to do. + final CloudFilesOperation operation; + + /// The folder to upload to, or the file to delete, relative to the + /// project's storage folder. May contain variable paths. + final String path; + + /// Which files the picker offers. + final CloudFilesType fileType; + + /// Whether the user may pick more than one file. + final bool allowMultiple; + + /// Largest file accepted, in megabytes. Larger files are skipped. + final num maxSizeMB; + + /// Map variable that receives the upload progress. + final VariableData? progressVariable; + + /// Actions to run after each file uploads, with `${result}` set to it. + final List actions; + + /// Creates a [CloudFilesAction]. + CloudFilesAction({ + this.operation = CloudFilesOperation.upload, + this.path = '', + this.fileType = CloudFilesType.image, + this.allowMultiple = true, + this.maxSizeMB = 10, + this.progressVariable, + List? actions, + super.nonBlocking, + super.enabled, + }) : actions = actions ?? [], + super(type: ActionType.cloudFiles); + + /// Duplicates this action with the given values replaced. + CloudFilesAction copyWith({ + CloudFilesOperation? operation, + String? path, + CloudFilesType? fileType, + bool? allowMultiple, + num? maxSizeMB, + VariableData? progressVariable, + List? actions, + }) => CloudFilesAction( + operation: operation ?? this.operation, + path: path ?? this.path, + fileType: fileType ?? this.fileType, + allowMultiple: allowMultiple ?? this.allowMultiple, + maxSizeMB: maxSizeMB ?? this.maxSizeMB, + progressVariable: progressVariable ?? this.progressVariable, + actions: actions ?? this.actions, + nonBlocking: nonBlocking, + enabled: enabled, + ); + + @override + List get props => [ + operation, + path, + fileType, + allowMultiple, + maxSizeMB, + progressVariable, + actions, + ]; + + /// Creates an instance from a JSON object. + factory CloudFilesAction.fromJson(Map json) => + _$CloudFilesActionFromJson(json); + + @override + Map toJson() => + _$CloudFilesActionOmitDefaults(this, _$CloudFilesActionToJson(this)); + + @override + R? accept(ActionVisitor visitor) => visitor.visitCloudFilesAction(this); +} diff --git a/lib/src/api/models/action/cloud_files_action.g.dart b/lib/src/api/models/action/cloud_files_action.g.dart new file mode 100644 index 0000000..405b506 --- /dev/null +++ b/lib/src/api/models/action/cloud_files_action.g.dart @@ -0,0 +1,92 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'cloud_files_action.dart'; + +// ************************************************************************** +// JsonDefaultsGenerator +// ************************************************************************** + +Map _$CloudFilesActionOmitDefaults( + CloudFilesAction instance, + Map json, +) { + if (isJsonDefault(instance.operation, CloudFilesOperation.upload)) + json.remove('operation'); + if (isJsonDefault(instance.path, '')) json.remove('path'); + if (isJsonDefault(instance.fileType, CloudFilesType.image)) + json.remove('fileType'); + if (isJsonDefault(instance.allowMultiple, true)) json.remove('allowMultiple'); + if (isJsonDefault(instance.maxSizeMB, 10)) json.remove('maxSizeMB'); + if (isJsonDefault(instance.nonBlocking, false)) json.remove('nonBlocking'); + if (isJsonDefault(instance.enabled, true)) json.remove('enabled'); + return json; +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +CloudFilesAction _$CloudFilesActionFromJson(Map json) => CloudFilesAction( + operation: + $enumDecodeNullable(_$CloudFilesOperationEnumMap, json['operation']) ?? + CloudFilesOperation.upload, + path: json['path'] as String? ?? '', + fileType: + $enumDecodeNullable(_$CloudFilesTypeEnumMap, json['fileType']) ?? + CloudFilesType.image, + allowMultiple: json['allowMultiple'] as bool? ?? true, + maxSizeMB: json['maxSizeMB'] as num? ?? 10, + progressVariable: json['progressVariable'] == null + ? null + : VariableData.fromJson( + Map.from(json['progressVariable'] as Map), + ), + actions: (json['actions'] as List?) + ?.map((e) => ActionModel.fromJson(e as Map)) + .toList(), + nonBlocking: json['nonBlocking'] as bool? ?? false, + enabled: json['enabled'] as bool? ?? true, +)..type = $enumDecode(_$ActionTypeEnumMap, json['type']); + +Map _$CloudFilesActionToJson(CloudFilesAction instance) => + { + 'type': _$ActionTypeEnumMap[instance.type]!, + 'nonBlocking': instance.nonBlocking, + 'enabled': instance.enabled, + 'operation': _$CloudFilesOperationEnumMap[instance.operation]!, + 'path': instance.path, + 'fileType': _$CloudFilesTypeEnumMap[instance.fileType]!, + 'allowMultiple': instance.allowMultiple, + 'maxSizeMB': instance.maxSizeMB, + 'progressVariable': ?instance.progressVariable?.toJson(), + 'actions': instance.actions.map((e) => e.toJson()).toList(), + }; + +const _$CloudFilesOperationEnumMap = { + CloudFilesOperation.upload: 'upload', + CloudFilesOperation.delete: 'delete', +}; + +const _$CloudFilesTypeEnumMap = { + CloudFilesType.image: 'image', + CloudFilesType.any: 'any', +}; + +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', +}; diff --git a/lib/src/api/models/action/conditional_action.dart b/lib/src/api/models/action/conditional_action.dart new file mode 100644 index 0000000..0f2d4f9 --- /dev/null +++ b/lib/src/api/models/action/conditional_action.dart @@ -0,0 +1,63 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import '../../mixins.dart'; +import '../models.dart'; + +part 'conditional_action.g.dart'; + +/// Runs [actions] when [expression] is true and [elseActions] otherwise. +/// +/// This is the logic builder's "if": it branches an action chain on a +/// variable, e.g. `${auth.isSignedIn}`, the result of a query, or a value a +/// previous action wrote. The expression is the same as a condition's. +@JsonSerializable() +class ConditionalAction extends ActionModel with Equatable, SerializableMixin { + /// The expression to test. + final BaseExpression expression; + + /// Actions to run, in order, when [expression] is true. + final List actions; + + /// Actions to run, in order, when [expression] is false. + final List elseActions; + + /// Creates a [ConditionalAction]. + ConditionalAction({ + required this.expression, + List? actions, + List? elseActions, + super.nonBlocking, + super.enabled, + }) : actions = actions ?? [], + elseActions = elseActions ?? [], + super(type: ActionType.conditional); + + /// Duplicates this action with the given values replaced. + ConditionalAction copyWith({ + BaseExpression? expression, + List? actions, + List? elseActions, + }) => ConditionalAction( + expression: expression ?? this.expression, + actions: actions ?? this.actions, + elseActions: elseActions ?? this.elseActions, + nonBlocking: nonBlocking, + enabled: enabled, + ); + + @override + List get props => [expression, actions, elseActions]; + + /// Creates an instance from a JSON object. + factory ConditionalAction.fromJson(Map json) => + _$ConditionalActionFromJson(json); + + @override + Map toJson() => + _$ConditionalActionOmitDefaults(this, _$ConditionalActionToJson(this)); + + @override + R? accept(ActionVisitor visitor) => + visitor.visitConditionalAction(this); +} diff --git a/lib/src/api/models/action/conditional_action.g.dart b/lib/src/api/models/action/conditional_action.g.dart new file mode 100644 index 0000000..712a95b --- /dev/null +++ b/lib/src/api/models/action/conditional_action.g.dart @@ -0,0 +1,63 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'conditional_action.dart'; + +// ************************************************************************** +// JsonDefaultsGenerator +// ************************************************************************** + +Map _$ConditionalActionOmitDefaults( + ConditionalAction instance, + Map json, +) { + if (isJsonDefault(instance.nonBlocking, false)) json.remove('nonBlocking'); + if (isJsonDefault(instance.enabled, true)) json.remove('enabled'); + return json; +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ConditionalAction _$ConditionalActionFromJson(Map json) => ConditionalAction( + expression: BaseExpression.fromJson( + Map.from(json['expression'] as Map), + ), + actions: (json['actions'] as List?) + ?.map((e) => ActionModel.fromJson(e as Map)) + .toList(), + elseActions: (json['elseActions'] as List?) + ?.map((e) => ActionModel.fromJson(e as Map)) + .toList(), + nonBlocking: json['nonBlocking'] as bool? ?? false, + enabled: json['enabled'] as bool? ?? true, +)..type = $enumDecode(_$ActionTypeEnumMap, json['type']); + +Map _$ConditionalActionToJson(ConditionalAction instance) => + { + 'type': _$ActionTypeEnumMap[instance.type]!, + 'nonBlocking': instance.nonBlocking, + 'enabled': instance.enabled, + 'expression': instance.expression.toJson(), + 'actions': instance.actions.map((e) => e.toJson()).toList(), + 'elseActions': instance.elseActions.map((e) => e.toJson()).toList(), + }; + +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', +}; diff --git a/lib/src/api/models/action/for_each_action.dart b/lib/src/api/models/action/for_each_action.dart new file mode 100644 index 0000000..654c8af --- /dev/null +++ b/lib/src/api/models/action/for_each_action.dart @@ -0,0 +1,52 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import '../../mixins.dart'; +import '../models.dart'; + +part 'for_each_action.g.dart'; + +/// Runs [actions] once for every item of a list. +/// +/// [items] is a variable path that resolves to a list, e.g. `${photos}`. +/// While the actions run, `${result.item}` is the current item and +/// `${result.index}` its index. +@JsonSerializable() +class ForEachAction extends ActionModel with Equatable, SerializableMixin { + /// A variable path that resolves to the list to iterate. + final String items; + + /// Actions to run, in order, for each item. + final List actions; + + /// Creates a [ForEachAction]. + ForEachAction({ + this.items = '', + List? actions, + super.nonBlocking, + super.enabled, + }) : actions = actions ?? [], + super(type: ActionType.forEach); + + /// Duplicates this action with the given values replaced. + ForEachAction copyWith({String? items, List? actions}) => + ForEachAction( + items: items ?? this.items, + actions: actions ?? this.actions, + nonBlocking: nonBlocking, + enabled: enabled, + ); + + @override + List get props => [items, actions]; + + /// Creates an instance from a JSON object. + factory ForEachAction.fromJson(Map json) => _$ForEachActionFromJson(json); + + @override + Map toJson() => + _$ForEachActionOmitDefaults(this, _$ForEachActionToJson(this)); + + @override + R? accept(ActionVisitor visitor) => visitor.visitForEachAction(this); +} diff --git a/lib/src/api/models/action/for_each_action.g.dart b/lib/src/api/models/action/for_each_action.g.dart new file mode 100644 index 0000000..da82e76 --- /dev/null +++ b/lib/src/api/models/action/for_each_action.g.dart @@ -0,0 +1,58 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'for_each_action.dart'; + +// ************************************************************************** +// JsonDefaultsGenerator +// ************************************************************************** + +Map _$ForEachActionOmitDefaults( + ForEachAction instance, + Map json, +) { + if (isJsonDefault(instance.items, '')) json.remove('items'); + if (isJsonDefault(instance.nonBlocking, false)) json.remove('nonBlocking'); + if (isJsonDefault(instance.enabled, true)) json.remove('enabled'); + return json; +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ForEachAction _$ForEachActionFromJson(Map json) => ForEachAction( + items: json['items'] as String? ?? '', + actions: (json['actions'] as List?) + ?.map((e) => ActionModel.fromJson(e as Map)) + .toList(), + nonBlocking: json['nonBlocking'] as bool? ?? false, + enabled: json['enabled'] as bool? ?? true, +)..type = $enumDecode(_$ActionTypeEnumMap, json['type']); + +Map _$ForEachActionToJson(ForEachAction instance) => + { + 'type': _$ActionTypeEnumMap[instance.type]!, + 'nonBlocking': instance.nonBlocking, + 'enabled': instance.enabled, + 'items': instance.items, + 'actions': instance.actions.map((e) => e.toJson()).toList(), + }; + +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', +}; diff --git a/lib/src/api/models/action/link_action.g.dart b/lib/src/api/models/action/link_action.g.dart index c2b969a..980c977 100644 --- a/lib/src/api/models/action/link_action.g.dart +++ b/lib/src/api/models/action/link_action.g.dart @@ -46,4 +46,8 @@ const _$ActionTypeEnumMap = { ActionType.setStorage: 'setStorage', ActionType.setCloudStorage: 'setCloudStorage', ActionType.loadFromCloudStorage: 'loadFromCloudStorage', + ActionType.auth: 'auth', + ActionType.conditional: 'conditional', + ActionType.forEach: 'forEach', + ActionType.cloudFiles: 'cloudFiles', }; 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 4cbd5d8..cffa3fe 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 @@ -89,4 +89,8 @@ const _$ActionTypeEnumMap = { ActionType.setStorage: 'setStorage', ActionType.setCloudStorage: 'setCloudStorage', ActionType.loadFromCloudStorage: 'loadFromCloudStorage', + ActionType.auth: 'auth', + ActionType.conditional: 'conditional', + ActionType.forEach: 'forEach', + ActionType.cloudFiles: 'cloudFiles', }; diff --git a/lib/src/api/models/action/navigation_action.g.dart b/lib/src/api/models/action/navigation_action.g.dart index d3b658e..a65dc37 100644 --- a/lib/src/api/models/action/navigation_action.g.dart +++ b/lib/src/api/models/action/navigation_action.g.dart @@ -60,4 +60,8 @@ const _$ActionTypeEnumMap = { ActionType.setStorage: 'setStorage', ActionType.setCloudStorage: 'setCloudStorage', ActionType.loadFromCloudStorage: 'loadFromCloudStorage', + ActionType.auth: 'auth', + ActionType.conditional: 'conditional', + ActionType.forEach: 'forEach', + ActionType.cloudFiles: 'cloudFiles', }; 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 4ce1fb2..8e1486f 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 @@ -97,6 +97,10 @@ const _$ActionTypeEnumMap = { ActionType.setStorage: 'setStorage', ActionType.setCloudStorage: 'setCloudStorage', ActionType.loadFromCloudStorage: 'loadFromCloudStorage', + ActionType.auth: 'auth', + ActionType.conditional: 'conditional', + ActionType.forEach: 'forEach', + ActionType.cloudFiles: 'cloudFiles', }; 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 e621bb4..017fd47 100644 --- a/lib/src/api/models/action/set_storage_action.g.dart +++ b/lib/src/api/models/action/set_storage_action.g.dart @@ -135,4 +135,8 @@ const _$ActionTypeEnumMap = { ActionType.setStorage: 'setStorage', ActionType.setCloudStorage: 'setCloudStorage', ActionType.loadFromCloudStorage: 'loadFromCloudStorage', + ActionType.auth: 'auth', + ActionType.conditional: 'conditional', + ActionType.forEach: 'forEach', + ActionType.cloudFiles: 'cloudFiles', }; 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 fbe93ea..dc7a9d3 100644 --- a/lib/src/api/models/action/set_value_action.g.dart +++ b/lib/src/api/models/action/set_value_action.g.dart @@ -112,6 +112,10 @@ const _$ActionTypeEnumMap = { ActionType.setStorage: 'setStorage', ActionType.setCloudStorage: 'setCloudStorage', ActionType.loadFromCloudStorage: 'loadFromCloudStorage', + ActionType.auth: 'auth', + ActionType.conditional: 'conditional', + ActionType.forEach: 'forEach', + ActionType.cloudFiles: 'cloudFiles', }; 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 af8d8ae..8859330 100644 --- a/lib/src/api/models/action/set_variable_action.g.dart +++ b/lib/src/api/models/action/set_variable_action.g.dart @@ -103,4 +103,8 @@ const _$ActionTypeEnumMap = { ActionType.setStorage: 'setStorage', ActionType.setCloudStorage: 'setCloudStorage', ActionType.loadFromCloudStorage: 'loadFromCloudStorage', + ActionType.auth: 'auth', + ActionType.conditional: 'conditional', + ActionType.forEach: 'forEach', + ActionType.cloudFiles: 'cloudFiles', }; 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 d42e343..e2b2f78 100644 --- a/lib/src/api/models/action/set_variant_action.g.dart +++ b/lib/src/api/models/action/set_variant_action.g.dart @@ -48,4 +48,8 @@ const _$ActionTypeEnumMap = { ActionType.setStorage: 'setStorage', ActionType.setCloudStorage: 'setCloudStorage', ActionType.loadFromCloudStorage: 'loadFromCloudStorage', + ActionType.auth: 'auth', + ActionType.conditional: 'conditional', + ActionType.forEach: 'forEach', + ActionType.cloudFiles: 'cloudFiles', }; 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 8f89df1..f42b52e 100644 --- a/lib/src/api/models/action/show_dialog_action.g.dart +++ b/lib/src/api/models/action/show_dialog_action.g.dart @@ -64,4 +64,8 @@ const _$ActionTypeEnumMap = { ActionType.setStorage: 'setStorage', ActionType.setCloudStorage: 'setCloudStorage', ActionType.loadFromCloudStorage: 'loadFromCloudStorage', + ActionType.auth: 'auth', + ActionType.conditional: 'conditional', + ActionType.forEach: 'forEach', + ActionType.cloudFiles: 'cloudFiles', }; diff --git a/lib/src/api/models/action/submit_action.g.dart b/lib/src/api/models/action/submit_action.g.dart index 5224597..4ce331d 100644 --- a/lib/src/api/models/action/submit_action.g.dart +++ b/lib/src/api/models/action/submit_action.g.dart @@ -64,6 +64,10 @@ const _$ActionTypeEnumMap = { ActionType.setStorage: 'setStorage', ActionType.setCloudStorage: 'setCloudStorage', ActionType.loadFromCloudStorage: 'loadFromCloudStorage', + ActionType.auth: 'auth', + ActionType.conditional: 'conditional', + ActionType.forEach: 'forEach', + ActionType.cloudFiles: 'cloudFiles', }; MailchimpSubmitAction _$MailchimpSubmitActionFromJson(Map json) => diff --git a/lib/src/api/models/condition.dart b/lib/src/api/models/condition.dart index e1e9c39..3822a4d 100644 --- a/lib/src/api/models/condition.dart +++ b/lib/src/api/models/condition.dart @@ -787,6 +787,18 @@ abstract interface class ActionVisitor { /// Evaluates the action and returns the result. R? visitLoadFromCloudStorageAction(LoadFromCloudStorageAction action); + + /// Evaluates the action and returns the result. + R? visitAuthAction(AuthAction action); + + /// Evaluates the action and returns the result. + R? visitConditionalAction(ConditionalAction action); + + /// Evaluates the action and returns the result. + R? visitForEachAction(ForEachAction action); + + /// Evaluates the action and returns the result. + R? visitCloudFilesAction(CloudFilesAction action); } /// An interface for evaluating expressions. diff --git a/lib/src/api/models/models.dart b/lib/src/api/models/models.dart index b4e984e..f12d72c 100644 --- a/lib/src/api/models/models.dart +++ b/lib/src/api/models/models.dart @@ -1,6 +1,10 @@ export '../json_defaults.dart'; export 'action/action.dart'; export 'action/api_call_action.dart'; +export 'action/auth_action.dart'; +export 'action/cloud_files_action.dart'; +export 'action/conditional_action.dart'; +export 'action/for_each_action.dart'; export 'action/call_function_action.dart'; export 'action/link_action.dart'; export 'action/load_from_cloud_storage_action.dart'; diff --git a/lib/src/api/models/paint.dart b/lib/src/api/models/paint.dart index b1d4348..8564475 100644 --- a/lib/src/api/models/paint.dart +++ b/lib/src/api/models/paint.dart @@ -572,7 +572,20 @@ class PaintModel with Equatable, SerializableMixin, FieldsHolder { ]; /// Factory constructor for creating [PaintModel] instance from a JSON data. - factory PaintModel.fromJson(Map json) => _$PaintModelFromJson(json); + factory PaintModel.fromJson(Map json) => + _$PaintModelFromJson(_alphaAsOpacity(json)); + + /// A solid color written as `#AARRGGBB` keeps its alpha as the paint's + /// opacity; [ColorRGB] has no alpha channel and would drop it. + static Map _alphaAsOpacity(Map json) { + final Object? color = json['color']; + if (json['opacity'] != null || color is! String) return json; + final String hex = color.replaceAll('#', ''); + if (hex.length != 8) return json; + final int? alpha = int.tryParse(hex.substring(0, 2), radix: 16); + if (alpha == null || alpha == 255) return json; + return {...json, 'opacity': alpha / 255}; + } @override Map toJson() => _$PaintModelOmitDefaults(this, _$PaintModelToJson(this)); diff --git a/lib/src/api/models/variables_model.dart b/lib/src/api/models/variables_model.dart index b916e67..d45edad 100644 --- a/lib/src/api/models/variables_model.dart +++ b/lib/src/api/models/variables_model.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:json_annotation/json_annotation.dart'; import 'package:equatable/equatable.dart'; @@ -55,6 +53,8 @@ enum VariableType { List() => VariableType.list, Map() => VariableType.map, String() => VariableType.text, + // Dates read as text, the way they are stored. + DateTime() => VariableType.text, _ => throw UnsupportedError( 'object type ${obj.runtimeType} is not supported. Cannot determine variable type', ), @@ -216,13 +216,13 @@ String? sanitizeValueForVariableType(Object? value, VariableType type) { return null; // This could be a bit expensive. Maybe enable only when required! case VariableType.map: - if (value is Map) return jsonEncode(value); + if (value is Map) return encodeJsonValue(value); final map = value.toMap(); - return map != null ? jsonEncode(map) : null; + return map != null ? encodeJsonValue(map) : null; case VariableType.list: - if (value is List) return jsonEncode(value); + if (value is List) return encodeJsonValue(value); final list = value.toList(); - return list != null ? jsonEncode(list) : null; + return list != null ? encodeJsonValue(list) : null; } } diff --git a/lib/src/api/nodes/row_column_node.dart b/lib/src/api/nodes/row_column_node.dart index a8f7424..2094d2f 100644 --- a/lib/src/api/nodes/row_column_node.dart +++ b/lib/src/api/nodes/row_column_node.dart @@ -8,7 +8,8 @@ part 'row_column_node.g.dart'; /// A node that can lay out its children in a row or column. /// Typically referred to [Row] and [Column] widgets in Flutter. @JsonSerializable() -class RowColumnNode extends ParentNode with RowColumnMixin, ScrollableMixin { +class RowColumnNode extends ParentNode + with RowColumnMixin, ScrollableMixin, CornerMixin { @override final String type = 'rowColumn'; @@ -62,6 +63,9 @@ class RowColumnNode extends ParentNode with RowColumnMixin, ScrollableMixin { super.reactions, super.parentID, super.clipsContent = false, + // [CornerMixin] properties. + CornerRadius cornerRadius = CornerRadius.zero, + double cornerSmoothing = 0, // [ScrollableMixin] properties. bool isScrollable = false, bool reverse = false, @@ -73,6 +77,11 @@ class RowColumnNode extends ParentNode with RowColumnMixin, ScrollableMixin { bool useFlutterListView = false, bool shouldAlwaysScroll = true, }) { + setCornerMixin( + cornerRadius: cornerRadius, + cornerSmoothing: cornerSmoothing, + ); + setRowColumnMixin( rowColumnType: rowColumnType, mainAxisAlignment: mainAxisAlignment, diff --git a/lib/src/api/nodes/row_column_node.g.dart b/lib/src/api/nodes/row_column_node.g.dart index 9a9ee51..8a53071 100644 --- a/lib/src/api/nodes/row_column_node.g.dart +++ b/lib/src/api/nodes/row_column_node.g.dart @@ -78,6 +78,10 @@ Map _$RowColumnNodeOmitDefaults( json.remove('useFlutterListView'); if (isJsonDefault(instance.shouldAlwaysScroll, true)) json.remove('shouldAlwaysScroll'); + if (isJsonDefault(instance.cornerSmoothing, 0)) + json.remove('cornerSmoothing'); + if (isJsonDefault(instance.cornerRadius, CornerRadius.zero)) + json.remove('cornerRadius'); return json; } @@ -197,6 +201,10 @@ RowColumnNode _$RowColumnNodeFromJson(Map json) => .toList() ?? const [], clipsContent: json['clipsContent'] as bool? ?? false, + cornerRadius: json['cornerRadius'] == null + ? CornerRadius.zero + : CornerRadius.fromJson(json['cornerRadius']), + cornerSmoothing: (json['cornerSmoothing'] as num?)?.toDouble() ?? 0, isScrollable: json['isScrollable'] as bool? ?? false, reverse: json['reverse'] as bool? ?? false, physics: @@ -292,6 +300,8 @@ Map _$RowColumnNodeToJson( 'dashPattern': instance.dashPattern, 'strokeSide': _$StrokeSideEnumMap[instance.strokeSide]!, 'clipsContent': instance.clipsContent, + 'cornerSmoothing': instance.cornerSmoothing, + 'cornerRadius': instance.cornerRadius.toJson(), 'rowColumnType': _$RowColumnTypeEnumMap[instance.rowColumnType]!, 'mainAxisAlignment': _$MainAxisAlignmentCEnumMap[instance.mainAxisAlignment]!, 'crossAxisAlignment': diff --git a/lib/src/api/typed_value.dart b/lib/src/api/typed_value.dart index 30c0e4c..f135784 100644 --- a/lib/src/api/typed_value.dart +++ b/lib/src/api/typed_value.dart @@ -23,7 +23,9 @@ extension ObjectExt on Object? { const (PaintModel) => value.toColorRGBA()?.toPaint().tryCast() ?? defaultValue, const (String) => - (value is List || value is Map ? jsonEncode(value) : value.toString()) + (value is List || value is Map + ? encodeJsonValue(value) + : value.toString()) .tryCast() ?? defaultValue, const (int) => value.toInt().tryCast() ?? defaultValue, diff --git a/lib/src/api/utils.dart b/lib/src/api/utils.dart index 10a9def..b165f17 100644 --- a/lib/src/api/utils.dart +++ b/lib/src/api/utils.dart @@ -1,8 +1,20 @@ +import 'dart:convert'; + import 'package:collection/collection.dart'; import 'package:path/path.dart' as p; import '../../codelessly_api.dart'; +/// Encodes [value] as JSON. Dates, such as the createdAt and updatedAt fields +/// of cloud documents, become ISO-8601 strings instead of failing the encode. +String encodeJsonValue(Object? value) => jsonEncode( + value, + toEncodable: (object) => switch (object) { + DateTime date => date.toIso8601String(), + _ => (object as dynamic).toJson(), + }, +); + /// Rounds the given [value] to the [precision]'th decimal place. double roundDoubleToJson(double value) => value.roundToPrecision(3); diff --git a/pubspec.yaml b/pubspec.yaml index b6ddaeb..3a6984f 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: 5.1.0 +version: 5.2.0 homepage: https://codelessly.com/ repository: https://github.com/Codelessly/CodelesslyAPI