diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index c8c8bf3..fae8f70 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -1,5 +1,7 @@ PODS: - Flutter (1.0.0) + - flutter_keyboard_visibility (0.0.1): + - Flutter - FMDB (2.7.5): - FMDB/standard (= 2.7.5) - FMDB/standard (2.7.5) @@ -15,6 +17,7 @@ PODS: DEPENDENCIES: - Flutter (from `Flutter`) + - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) - shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`) @@ -27,6 +30,8 @@ SPEC REPOS: EXTERNAL SOURCES: Flutter: :path: Flutter + flutter_keyboard_visibility: + :path: ".symlinks/plugins/flutter_keyboard_visibility/ios" path_provider_ios: :path: ".symlinks/plugins/path_provider_ios/ios" shared_preferences_ios: @@ -38,6 +43,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a + flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5 shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad diff --git a/mobile/lib/src/views/create_pipeline_page.dart b/mobile/lib/src/views/create_pipeline_page.dart index 5f4ea17..a0bae4c 100644 --- a/mobile/lib/src/views/create_pipeline_page.dart +++ b/mobile/lib/src/views/create_pipeline_page.dart @@ -79,8 +79,10 @@ class _CreatePipelinePageState extends State { onTap: () { showAerisCardPage( context, - (_) => - SetupActionPage(action: trigger)) + (_) => SetupActionPage( + action: trigger, + parentReactions: reactions + )) .then((_) => setState(() {})); }) : ActionCard( @@ -88,6 +90,8 @@ class _CreatePipelinePageState extends State { title: trigger.displayName(), trailing: ActionCardPopupMenu( deletable: false, + parentReactions: reactions, + parentTrigger: trigger, action: trigger, then: () => setState(() {})), ), @@ -106,6 +110,8 @@ class _CreatePipelinePageState extends State { leading: reaction.service.getLogo(logoSize: 50), title: reaction.displayName(), trailing: ActionCardPopupMenu( + parentTrigger: trigger == Trigger.template() ? null : trigger, + parentReactions: reactions, deletable: reactions.length > 1, action: reaction, then: () => setState(() {}), @@ -128,7 +134,10 @@ class _CreatePipelinePageState extends State { showAerisCardPage( context, (_) => SetupActionPage( - action: newreact)) + action: newreact, + parentReactions: reactions, + parentTrigger: trigger == Trigger.template() ? null : trigger, + )) .then((_) => setState(() { if (newreact != Reaction.template()) { reactions.add(newreact); diff --git a/mobile/lib/src/views/pipeline_detail_page.dart b/mobile/lib/src/views/pipeline_detail_page.dart index 50010d8..4c5aeca 100644 --- a/mobile/lib/src/views/pipeline_detail_page.dart +++ b/mobile/lib/src/views/pipeline_detail_page.dart @@ -95,7 +95,12 @@ class _PipelineDetailPageState extends State { onTap: () { Reaction newreaction = Reaction.template(); showAerisCardPage( - context, (_) => SetupActionPage(action: newreaction)) + context, (_) => SetupActionPage( + action: newreaction, + parentTrigger: pipeline.trigger, + parentReactions: pipeline.reactions, + ) + ) .then((r) { if (newreaction != Reaction.template()) { setState(() { @@ -138,6 +143,8 @@ class _PipelineDetailPageState extends State { title: pipeline.trigger.displayName(), trailing: ActionCardPopupMenu( deletable: false, + parentTrigger: pipeline.trigger, + parentReactions: pipeline.reactions, action: pipeline.trigger, then: () { setState(() {}); @@ -154,6 +161,8 @@ class _PipelineDetailPageState extends State { leading: reaction.service.getLogo(logoSize: 50), title: reaction.displayName(), trailing: ActionCardPopupMenu( + parentTrigger: pipeline.trigger, + parentReactions: pipeline.reactions, deletable: pipeline.reactions.length > 1, action: reaction, then: () { diff --git a/mobile/lib/src/views/setup_action_page.dart b/mobile/lib/src/views/setup_action_page.dart index 45abba6..7bc1657 100644 --- a/mobile/lib/src/views/setup_action_page.dart +++ b/mobile/lib/src/views/setup_action_page.dart @@ -1,6 +1,7 @@ import 'package:aeris/src/models/action_parameter.dart'; import 'package:aeris/src/models/action_template.dart'; import 'package:aeris/src/aeris_api.dart'; +import 'package:aeris/src/models/reaction.dart'; import 'package:aeris/src/models/trigger.dart'; import 'package:flutter/material.dart'; import 'package:aeris/src/models/action.dart' as aeris; @@ -14,10 +15,15 @@ import 'package:skeleton_loader/skeleton_loader.dart'; ///Page to setup an action class SetupActionPage extends StatefulWidget { - const SetupActionPage({Key? key, required this.action}) : super(key: key); + const SetupActionPage({Key? key, required this.action, required this.parentReactions, this.parentTrigger}) : super(key: key); /// Action to setup final aeris.Action action; + /// Trigger of Parent of the action to setup + final Trigger? parentTrigger; + + /// reactions of Parent of the action to setup + final List parentReactions; @override State createState() => _SetupActionPageState(); @@ -120,6 +126,9 @@ class _SetupActionPageState extends State { expanded: Padding( padding: const EdgeInsets.all(20), child: ActionForm( + reactionsCandidates: widget.parentReactions, + triggerCandidate: widget.parentTrigger, + candidate: widget.action, key: Key("${availableAction.name}${availableAction.description}${availableAction.service}"), description: availableAction.description!, name: availableAction.name, diff --git a/mobile/lib/src/widgets/action_card_popup_menu.dart b/mobile/lib/src/widgets/action_card_popup_menu.dart index 89d5c6c..e3ed10d 100644 --- a/mobile/lib/src/widgets/action_card_popup_menu.dart +++ b/mobile/lib/src/widgets/action_card_popup_menu.dart @@ -1,3 +1,5 @@ +import 'package:aeris/src/models/reaction.dart'; +import 'package:aeris/src/models/trigger.dart'; import 'package:aeris/src/widgets/aeris_card_page.dart'; import 'package:flutter/material.dart'; import 'package:aeris/src/views/setup_action_page.dart'; @@ -11,6 +13,8 @@ class ActionCardPopupMenu extends StatelessWidget { ActionCardPopupMenu({ Key? key, required this.action, + this.parentTrigger, + required this.parentReactions, required this.then, required this.deletable, this.onDelete, @@ -22,6 +26,10 @@ class ActionCardPopupMenu extends StatelessWidget { /// Selected Action final aeris.Action action; + /// Trigger of the Parent of the action + final Trigger? parentTrigger; + /// Trigger of the Parent of the action + final List parentReactions; /// Function to trigger once the Edit menu is closed final void Function() then; @@ -46,15 +54,18 @@ class ActionCardPopupMenu extends StatelessWidget { icon: Icons.settings, title: AppLocalizations.of(context).modify, value: () => showAerisCardPage( - context, (_) => SetupActionPage(action: action)).then((_) => then()) - ), + context, (_) => SetupActionPage( + action: action, + parentTrigger: parentTrigger, + parentReactions: parentReactions, + )) + .then((_) => then())), AerisPopupMenuItem( - context: context, - icon: Icons.delete, - title: AppLocalizations.of(context).delete, - value: onDelete, - enabled: deletable - ), + context: context, + icon: Icons.delete, + title: AppLocalizations.of(context).delete, + value: onDelete, + enabled: deletable), ]); } } diff --git a/mobile/lib/src/widgets/action_form.dart b/mobile/lib/src/widgets/action_form.dart index deed3eb..a8a8085 100644 --- a/mobile/lib/src/widgets/action_form.dart +++ b/mobile/lib/src/widgets/action_form.dart @@ -1,9 +1,16 @@ import 'package:aeris/src/models/action_parameter.dart'; +import 'package:aeris/src/models/action_template.dart'; +import 'package:aeris/src/models/reaction.dart'; +import 'package:aeris/src/models/action.dart' as aeris; +import 'package:aeris/src/models/trigger.dart'; +import 'package:aeris/src/providers/action_catalogue_provider.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:provider/provider.dart'; import 'package:recase/recase.dart'; +import 'package:tuple/tuple.dart'; /// Form for an action class ActionForm extends StatefulWidget { @@ -14,6 +21,13 @@ class ActionForm extends StatefulWidget { /// What the action does final String description; + /// The Action that will be eventually filled by the form + final aeris.Action candidate; + /// The trigger candidate in the parent form page + final Trigger? triggerCandidate; + /// The trigger candidate in the parent form page + final List reactionsCandidates; + /// On validate callback final void Function(Map) onValidate; @@ -22,7 +36,11 @@ class ActionForm extends StatefulWidget { required this.name, required this.description, required this.parameters, - required this.onValidate}) + required this.onValidate, + required this.candidate, + this.triggerCandidate, + required this.reactionsCandidates, + }) : super(key: key); @override @@ -30,34 +48,93 @@ class ActionForm extends StatefulWidget { } class _ActionFormState extends State { - final _formKey = GlobalKey(); + final _formKey = GlobalKey(); + Map formValues = {}; + + List getSuggestions(String pattern, ActionCatalogueProvider catalogue) { + List> suggestions = []; + if (pattern.endsWith("#") == false) return suggestions; + print(widget.candidate.runtimeType); + if (widget.candidate is Trigger) return suggestions; + if (widget.triggerCandidate != null) { + Trigger trigger = widget.triggerCandidate!; + ///TODO Dumb ass; look for returns instead of param + var triggerTemplate = catalogue.triggerTemplates[trigger.service]!.firstWhere( + (element) => element.name == trigger.name + ); + for (var parameter in triggerTemplate.returnedValues) { + suggestions.add(Tuple3(0, parameter, triggerTemplate)); + } + } + int index = 1; + for (var reactionCandidate in widget.reactionsCandidates) { + var reactionTemplate = catalogue.triggerTemplates[reactionCandidate.service]!.firstWhere( + (element) => element.name == reactionCandidate.name + ); + for (var parameter in reactionTemplate.returnedValues) { + suggestions.add(Tuple3(index, parameter, reactionTemplate)); + } + index++; + } + print(index); + suggestions.forEach((element) => print(element.item2.name)); + return suggestions; + } @override Widget build(BuildContext context) { - return FormBuilder( + return Consumer( + builder: (__, catalogue, _) => Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ Text(widget.description, textAlign: TextAlign.left, style: TextStyle(color: Theme.of(context).colorScheme.onSurface)), - ...widget.parameters.map((param) => FormBuilderTextField( - initialValue: param.value?.toString(), - name: param.name, - decoration: InputDecoration( - labelText: ReCase(param.name).titleCase, - helperText: param.description - ), - validator: FormBuilderValidators.compose([ - FormBuilderValidators.required(context), - ]), - )), + ...widget.parameters.map((param) { + return TypeAheadFormField( + initialValue: formValues[param.name] ?? param.value?.toString(), + validator: FormBuilderValidators.compose([ + FormBuilderValidators.required(context), + ]), + hideOnEmpty: true, + textFieldConfiguration: TextFieldConfiguration( + decoration: InputDecoration( + labelText: ReCase(param.name).titleCase, + helperText: param.description + ), + ), + suggestionsCallback: (suggestion) => getSuggestions(suggestion, catalogue), + noItemsFoundBuilder: (_) => Container(), + onSuggestionSelected: (suggestion) { + var parameterSuggestion = suggestion as Tuple3; + String content = formValues[param.name]!; + content += "{${parameterSuggestion.item2}@${parameterSuggestion.item1}}"; + print(content); + setState(() { + formValues[param.name] = content; + }); + }, + itemBuilder: (context, input) { + var suggestion = input as Tuple3; + return ListTile( + isThreeLine: true, + leading: suggestion.item3.service.getLogo(logoSize: 30), + title: Text(suggestion.item2.name), + subtitle: Text("${suggestion.item2.description}, from '${suggestion.item3.displayName()}'") + ); + }, + onSaved: (value) => setState(() { + formValues[param.name] = value!; + }), + ); + }), ...[ ElevatedButton( child: Text(AppLocalizations.of(context).save), onPressed: () { - _formKey.currentState!.save(); if (_formKey.currentState!.validate()) { - widget.onValidate(_formKey.currentState!.value.map((key, value) => MapEntry(key, value))); + _formKey.currentState!.save(); + widget.onValidate(formValues.map((key, value) => MapEntry(key, value))); } }, ), @@ -65,6 +142,6 @@ class _ActionFormState extends State { ] ] ) - ); + )); } }