From 46961a6fd939efd812a8a67654ef21fa06d9e4d5 Mon Sep 17 00:00:00 2001 From: pavanpodila Date: Wed, 27 Mar 2024 20:49:35 +0530 Subject: [PATCH] Update dependencies, refactor error view and add new network plugin This commit updates the project's dependencies, refactors how error views are managed and adds a new HTTP network plugin. It also adjusts some file and variable names for improved clarity. Coding conventions and organization have been improved for greater code readability and maintainability. Additionally, several functionalities have been enhanced, such as actions now being able to return Future or void. --- .../lib/plugin_types/{ => di}/di_plugin.dart | 0 .../{ => di}/plugin_di_get_it.dart | 0 .../{ => logger}/console_logger_plugin.dart | 0 .../{ => logger}/logger_plugin.dart | 0 .../network/http_network_plugin.dart | 60 +++++++++ .../plugin_types/network/network_plugin.dart | 25 ++++ .../runtime/platform/default_platform.dart | 3 +- .../default_platform_widget_builder.dart | 4 +- .../lib/runtime/platform/error_view.dart | 78 ++++++++++- .../runtime/platform/framework_init_view.dart | 3 +- .../lib/runtime/platform/vyuh_platform.dart | 3 + .../lib/runtime/platform_widget_builder.dart | 1 - packages/system/vyuh_core/lib/vyuh_core.dart | 10 +- packages/system/vyuh_core/pubspec.yaml | 1 + .../lib/content/action.dart | 8 +- .../lib/content_extension_builder.dart | 12 +- .../lib/ui/route_builder.dart | 4 +- .../lib/action/conditional_action.dart | 2 +- .../lib/api_handler/simple_api_handler.dart | 114 ++++++++++++++++ .../lib/api_handler/simple_api_handler.g.dart | 47 +++++++ .../lib/content/api_content.dart | 123 ++++++++++++++++++ .../lib/content/api_content.g.dart | 13 ++ .../lib/content/card/card.dart | 2 + .../lib/content/card/card.g.dart | 3 + .../lib/content/card/default_layout.dart | 3 +- .../lib/content/card/list_item_layout.dart | 67 +++++----- .../lib/content/index.dart | 1 + .../vyuh_feature_system/lib/feature.dart | 8 +- .../lib/vyuh_feature_system.dart | 1 + .../system/vyuh_feature_system/pubspec.yaml | 1 + 30 files changed, 540 insertions(+), 57 deletions(-) rename packages/system/vyuh_core/lib/plugin_types/{ => di}/di_plugin.dart (100%) rename packages/system/vyuh_core/lib/plugin_types/{ => di}/plugin_di_get_it.dart (100%) rename packages/system/vyuh_core/lib/plugin_types/{ => logger}/console_logger_plugin.dart (100%) rename packages/system/vyuh_core/lib/plugin_types/{ => logger}/logger_plugin.dart (100%) create mode 100644 packages/system/vyuh_core/lib/plugin_types/network/http_network_plugin.dart create mode 100644 packages/system/vyuh_core/lib/plugin_types/network/network_plugin.dart create mode 100644 packages/system/vyuh_feature_system/lib/api_handler/simple_api_handler.dart create mode 100644 packages/system/vyuh_feature_system/lib/api_handler/simple_api_handler.g.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/api_content.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/api_content.g.dart diff --git a/packages/system/vyuh_core/lib/plugin_types/di_plugin.dart b/packages/system/vyuh_core/lib/plugin_types/di/di_plugin.dart similarity index 100% rename from packages/system/vyuh_core/lib/plugin_types/di_plugin.dart rename to packages/system/vyuh_core/lib/plugin_types/di/di_plugin.dart diff --git a/packages/system/vyuh_core/lib/plugin_types/plugin_di_get_it.dart b/packages/system/vyuh_core/lib/plugin_types/di/plugin_di_get_it.dart similarity index 100% rename from packages/system/vyuh_core/lib/plugin_types/plugin_di_get_it.dart rename to packages/system/vyuh_core/lib/plugin_types/di/plugin_di_get_it.dart diff --git a/packages/system/vyuh_core/lib/plugin_types/console_logger_plugin.dart b/packages/system/vyuh_core/lib/plugin_types/logger/console_logger_plugin.dart similarity index 100% rename from packages/system/vyuh_core/lib/plugin_types/console_logger_plugin.dart rename to packages/system/vyuh_core/lib/plugin_types/logger/console_logger_plugin.dart diff --git a/packages/system/vyuh_core/lib/plugin_types/logger_plugin.dart b/packages/system/vyuh_core/lib/plugin_types/logger/logger_plugin.dart similarity index 100% rename from packages/system/vyuh_core/lib/plugin_types/logger_plugin.dart rename to packages/system/vyuh_core/lib/plugin_types/logger/logger_plugin.dart diff --git a/packages/system/vyuh_core/lib/plugin_types/network/http_network_plugin.dart b/packages/system/vyuh_core/lib/plugin_types/network/http_network_plugin.dart new file mode 100644 index 00000000..f843e171 --- /dev/null +++ b/packages/system/vyuh_core/lib/plugin_types/network/http_network_plugin.dart @@ -0,0 +1,60 @@ +import 'dart:convert'; + +import 'package:http/http.dart'; +import 'package:vyuh_core/vyuh_core.dart'; + +final class HttpNetworkPlugin extends NetworkPlugin { + late Client _client; + var _initialized = false; + + HttpNetworkPlugin() + : super(name: 'vyuh.plugin.network.http', title: 'HTTP Network Plugin'); + + @override + Future init() async { + if (_initialized) { + return; + } + + _client = Client(); + _initialized = true; + } + + @override + Future dispose() async { + if (!_initialized) { + return; + } + + _client.close(); + _initialized = false; + } + + @override + Future get(Uri url, {Map? headers}) => + _client.get(url, headers: headers); + + @override + Future head(Uri url, {Map? headers}) => + _client.head(url, headers: headers); + + @override + Future post(Uri url, + {Map? headers, Object? body, Encoding? encoding}) => + _client.post(url, headers: headers, body: body, encoding: encoding); + + @override + Future put(Uri url, + {Map? headers, Object? body, Encoding? encoding}) => + _client.put(url, headers: headers, body: body, encoding: encoding); + + @override + Future delete(Uri url, + {Map? headers, Object? body, Encoding? encoding}) => + _client.delete(url, headers: headers, body: body, encoding: encoding); + + @override + Future patch(Uri url, + {Map? headers, Object? body, Encoding? encoding}) => + _client.patch(url, headers: headers, body: body, encoding: encoding); +} diff --git a/packages/system/vyuh_core/lib/plugin_types/network/network_plugin.dart b/packages/system/vyuh_core/lib/plugin_types/network/network_plugin.dart new file mode 100644 index 00000000..237dd1e4 --- /dev/null +++ b/packages/system/vyuh_core/lib/plugin_types/network/network_plugin.dart @@ -0,0 +1,25 @@ +import 'dart:convert'; + +import 'package:http/http.dart'; +import 'package:vyuh_core/vyuh_core.dart'; + +abstract base class NetworkPlugin extends Plugin { + NetworkPlugin({required super.name, required super.title}) + : super(pluginType: PluginType.network); + + Future get(Uri url, {Map? headers}); + + Future head(Uri url, {Map? headers}); + + Future post(Uri url, + {Map? headers, Object? body, Encoding? encoding}); + + Future put(Uri url, + {Map? headers, Object? body, Encoding? encoding}); + + Future delete(Uri url, + {Map? headers, Object? body, Encoding? encoding}); + + Future patch(Uri url, + {Map? headers, Object? body, Encoding? encoding}); +} diff --git a/packages/system/vyuh_core/lib/runtime/platform/default_platform.dart b/packages/system/vyuh_core/lib/runtime/platform/default_platform.dart index a08c6523..69a7a6fd 100644 --- a/packages/system/vyuh_core/lib/runtime/platform/default_platform.dart +++ b/packages/system/vyuh_core/lib/runtime/platform/default_platform.dart @@ -172,8 +172,8 @@ final class DefaultVyuhPlatform extends VyuhPlatform { debugLogDiagnostics: kDebugMode, observers: analytics.observers, errorBuilder: (_, state) => widgetBuilder.routeErrorView( - path: state.matchedLocation, title: 'Failed to load route', + subtitle: state.matchedLocation, error: state.error, onRetry: () { vyuh.tracker.init(tracker.currentState.value); @@ -230,6 +230,7 @@ extension on PluginType { AnalyticsPlugin(providers: [NoOpAnalyticsProvider()]), PluginType.content => NoOpContentPlugin(), PluginType.di => GetItDIPlugin(), + PluginType.network => HttpNetworkPlugin(), _ => null }; } diff --git a/packages/system/vyuh_core/lib/runtime/platform/default_platform_widget_builder.dart b/packages/system/vyuh_core/lib/runtime/platform/default_platform_widget_builder.dart index 164f5336..9cecc5df 100644 --- a/packages/system/vyuh_core/lib/runtime/platform/default_platform_widget_builder.dart +++ b/packages/system/vyuh_core/lib/runtime/platform/default_platform_widget_builder.dart @@ -51,17 +51,15 @@ final defaultPlatformWidgetBuilder = PlatformWidgetBuilder( error: error, retryLabel: retryLabel, onRetry: onRetry, - showRestart: showRestart, ), routeErrorView: ({ - required path, required title, onRetry, retryLabel, subtitle, error, }) => - ErrorView( + ErrorViewScaffold( title: title, subtitle: subtitle, error: error, diff --git a/packages/system/vyuh_core/lib/runtime/platform/error_view.dart b/packages/system/vyuh_core/lib/runtime/platform/error_view.dart index a46e8ffb..059408af 100644 --- a/packages/system/vyuh_core/lib/runtime/platform/error_view.dart +++ b/packages/system/vyuh_core/lib/runtime/platform/error_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:vyuh_core/vyuh_core.dart'; -class ErrorView extends StatelessWidget { +class ErrorViewScaffold extends StatelessWidget { final String title; final String? subtitle; final dynamic error; @@ -10,7 +10,7 @@ class ErrorView extends StatelessWidget { final String? retryLabel; - const ErrorView({ + const ErrorViewScaffold({ super.key, this.title = 'Something is not right!', this.subtitle, @@ -42,6 +42,7 @@ class ErrorView extends StatelessWidget { color: textColor, size: 64, ), + Text(title, textAlign: TextAlign.center), if (subtitle != null) Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), @@ -90,3 +91,76 @@ class ErrorView extends StatelessWidget { ); } } + +class ErrorView extends StatelessWidget { + final String title; + final String? subtitle; + final dynamic error; + final VoidCallback? onRetry; + final String? retryLabel; + + const ErrorView({ + super.key, + this.title = 'Something is not right!', + this.subtitle, + this.error, + this.onRetry, + this.retryLabel, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + final textColor = theme.colorScheme.onSurface; + return ConstrainedBox( + constraints: const BoxConstraints(minHeight: 100, maxHeight: 200), + child: Card( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Icon( + Icons.hide_source_rounded, + color: textColor, + size: 32, + ), + Text(title, textAlign: TextAlign.center), + if (subtitle != null) + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text(subtitle!, + textAlign: TextAlign.center, + style: + theme.textTheme.titleMedium?.apply(color: textColor)), + ), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Column( + children: [ + if (error != null) + Text( + error.toString(), + style: theme.textTheme.bodyMedium?.apply( + fontFamily: 'Courier', + fontWeightDelta: 2, + fontSizeDelta: 1, + color: textColor, + ), + ), + ], + ), + )), + if (onRetry != null) + Padding( + padding: const EdgeInsets.only(left: 20.0, right: 20, top: 20), + child: FilledButton( + onPressed: onRetry, child: Text(retryLabel ?? 'Retry')), + ), + ], + ), + ), + ); + } +} diff --git a/packages/system/vyuh_core/lib/runtime/platform/framework_init_view.dart b/packages/system/vyuh_core/lib/runtime/platform/framework_init_view.dart index d920d560..c77abc8c 100644 --- a/packages/system/vyuh_core/lib/runtime/platform/framework_init_view.dart +++ b/packages/system/vyuh_core/lib/runtime/platform/framework_init_view.dart @@ -43,14 +43,13 @@ class _FrameworkInitViewState extends State { final child = status == null || status == FutureStatus.pending ? vyuh.widgetBuilder.appLoader() - : vyuh.widgetBuilder.errorView( + : vyuh.widgetBuilder.routeErrorView( title: 'Failed to load app', error: vyuh.tracker.error, retryLabel: 'Try Again', onRetry: () { vyuh.tracker.init(); }, - showRestart: false, ); final pendingApp = MaterialApp(home: child); diff --git a/packages/system/vyuh_core/lib/runtime/platform/vyuh_platform.dart b/packages/system/vyuh_core/lib/runtime/platform/vyuh_platform.dart index f5bab6b1..73ba46e3 100644 --- a/packages/system/vyuh_core/lib/runtime/platform/vyuh_platform.dart +++ b/packages/system/vyuh_core/lib/runtime/platform/vyuh_platform.dart @@ -29,6 +29,7 @@ abstract class VyuhPlatform { PluginType.content, PluginType.di, PluginType.analytics, + PluginType.network, // PluginType.storage, ]; @@ -58,6 +59,8 @@ extension NamedPlugins on VyuhPlatform { AnalyticsPlugin get analytics => ensurePlugin(PluginType.analytics); + NetworkPlugin get network => ensurePlugin(PluginType.network); + T ensurePlugin(PluginType type, {bool mustExist = true}) { final plugin = getPlugin(type) as T; diff --git a/packages/system/vyuh_core/lib/runtime/platform_widget_builder.dart b/packages/system/vyuh_core/lib/runtime/platform_widget_builder.dart index 9b93f25a..30b7436f 100644 --- a/packages/system/vyuh_core/lib/runtime/platform_widget_builder.dart +++ b/packages/system/vyuh_core/lib/runtime/platform_widget_builder.dart @@ -7,7 +7,6 @@ typedef RouteLoader = Widget Function([Uri? url, String? routeId]); typedef ImagePlaceholderBuilder = Widget Function( {double? width, double? height}); typedef RouteErrorViewBuilder = Widget Function({ - required String path, required String title, String? retryLabel, VoidCallback? onRetry, diff --git a/packages/system/vyuh_core/lib/vyuh_core.dart b/packages/system/vyuh_core/lib/vyuh_core.dart index e4ae6a1b..50c46adf 100644 --- a/packages/system/vyuh_core/lib/vyuh_core.dart +++ b/packages/system/vyuh_core/lib/vyuh_core.dart @@ -11,14 +11,16 @@ export 'feature_descriptor.dart'; export 'plugin_types/analytics/analytics_plugin.dart'; export 'plugin_types/analytics/analytics_provider.dart'; export 'plugin_types/analytics/noop_analytics_provider.dart'; -export 'plugin_types/console_logger_plugin.dart'; export 'plugin_types/content/content_plugin.dart'; export 'plugin_types/content/content_provider.dart'; export 'plugin_types/content/noop_content_provider.dart'; -export 'plugin_types/di_plugin.dart'; -export 'plugin_types/logger_plugin.dart'; +export 'plugin_types/di/di_plugin.dart'; +export 'plugin_types/di/plugin_di_get_it.dart'; +export 'plugin_types/logger/console_logger_plugin.dart'; +export 'plugin_types/logger/logger_plugin.dart'; +export 'plugin_types/network/http_network_plugin.dart'; +export 'plugin_types/network/network_plugin.dart'; export 'plugin_types/plugin.dart'; -export 'plugin_types/plugin_di_get_it.dart'; export 'runtime/cms_route.dart'; export 'runtime/init_tracker.dart'; export 'runtime/platform/default_platform_widget_builder.dart'; diff --git a/packages/system/vyuh_core/pubspec.yaml b/packages/system/vyuh_core/pubspec.yaml index a0856bc3..4ab17d60 100644 --- a/packages/system/vyuh_core/pubspec.yaml +++ b/packages/system/vyuh_core/pubspec.yaml @@ -21,6 +21,7 @@ dependencies: get_it: ^7.6.4 logger: ^2.1.0 flutter_sanity_portable_text: ^1.0.0-beta.4 + http: ^1.2.1 dev_dependencies: flutter_test: diff --git a/packages/system/vyuh_extension_content/lib/content/action.dart b/packages/system/vyuh_extension_content/lib/content/action.dart index f187c457..cff00aa5 100644 --- a/packages/system/vyuh_extension_content/lib/content/action.dart +++ b/packages/system/vyuh_extension_content/lib/content/action.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:vyuh_core/vyuh_core.dart'; @@ -14,9 +16,9 @@ final class Action { static configurationList(dynamic json) => listFromJson(json); - void execute(BuildContext context) { + FutureOr execute(BuildContext context) async { for (final config in configurations ?? []) { - config.execute(context); + await config.execute(context); } } @@ -37,5 +39,5 @@ abstract class ActionConfiguration { this.title, }); - void execute(BuildContext context); + FutureOr execute(BuildContext context); } diff --git a/packages/system/vyuh_extension_content/lib/content_extension_builder.dart b/packages/system/vyuh_extension_content/lib/content_extension_builder.dart index 162287ec..538d38f5 100644 --- a/packages/system/vyuh_extension_content/lib/content_extension_builder.dart +++ b/packages/system/vyuh_extension_content/lib/content_extension_builder.dart @@ -24,6 +24,7 @@ final class ContentExtensionBuilder extends ExtensionBuilder { .expand((element) => element.contents ?? []) .groupListsBy((element) => element.schemaType); + // Collect the builders for (final entry in contentBuilders.entries) { assert(entry.value.length == 1, 'There can be only one ContentBuilder for a content-type. We found ${entry.value.length} for ${entry.key}'); @@ -31,15 +32,22 @@ final class ContentExtensionBuilder extends ExtensionBuilder { _contentBuilderMap[entry.key] = entry.value.first; } + // Ensure every ContentDescriptor has a ContentBuilder for (final entry in contents.entries) { final schemaType = entry.key; - final descriptors = entry.value; final builder = _contentBuilderMap[schemaType]; assert(builder != null, 'Missing ContentBuilder for ContentDescriptor of schemaType: $schemaType'); + } + + // Setup the builders + for (final entry in _contentBuilderMap.entries) { + final schemaType = entry.key; + final builder = entry.value; + final descriptors = contents[schemaType] ?? []; - builder?.init(descriptors); + builder.init(descriptors); } _initTypeRegistrations( diff --git a/packages/system/vyuh_extension_content/lib/ui/route_builder.dart b/packages/system/vyuh_extension_content/lib/ui/route_builder.dart index e318e748..36e08888 100644 --- a/packages/system/vyuh_extension_content/lib/ui/route_builder.dart +++ b/packages/system/vyuh_extension_content/lib/ui/route_builder.dart @@ -73,7 +73,7 @@ class _RouteFutureBuilderState extends State { vyuh.analytics.reportError(exception); - return vyuh.widgetBuilder.errorView( + return vyuh.widgetBuilder.routeErrorView( title: 'Failed to load route from CMS', error: exception, onRetry: _refresh, @@ -88,7 +88,7 @@ class _RouteFutureBuilderState extends State { case FutureStatus.rejected: vyuh.analytics.reportError(_tracker.value?.error); - return vyuh.widgetBuilder.errorView( + return vyuh.widgetBuilder.routeErrorView( title: errorMsg, error: _tracker.value?.error, onRetry: _refresh, diff --git a/packages/system/vyuh_feature_system/lib/action/conditional_action.dart b/packages/system/vyuh_feature_system/lib/action/conditional_action.dart index a1dc8ced..a1d3322d 100644 --- a/packages/system/vyuh_feature_system/lib/action/conditional_action.dart +++ b/packages/system/vyuh_feature_system/lib/action/conditional_action.dart @@ -29,7 +29,7 @@ class ConditionalAction extends ActionConfiguration { _$ConditionalActionFromJson(json); @override - void execute(flutter.BuildContext context) async { + Future execute(flutter.BuildContext context) async { final value = (await condition?.execute()) ?? defaultCase; if (context.mounted) { diff --git a/packages/system/vyuh_feature_system/lib/api_handler/simple_api_handler.dart b/packages/system/vyuh_feature_system/lib/api_handler/simple_api_handler.dart new file mode 100644 index 00000000..f686fa55 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/api_handler/simple_api_handler.dart @@ -0,0 +1,114 @@ +import 'dart:convert'; + +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:json_path/json_path.dart'; +import 'package:vyuh_core/vyuh_core.dart'; +import 'package:vyuh_feature_system/content/card/list_item_layout.dart'; +import 'package:vyuh_feature_system/vyuh_feature_system.dart' as vf; +import 'package:vyuh_feature_system/vyuh_feature_system.dart'; + +part 'simple_api_handler.g.dart'; + +@JsonSerializable() +final class DisplayFieldMap { + final JSONPath? title; + final JSONPath? description; + final JSONPath? imageUrl; + + DisplayFieldMap({this.title, this.description, this.imageUrl}); + + factory DisplayFieldMap.fromJson(Map json) => + _$DisplayFieldMapFromJson(json); +} + +extension type const JSONPath(String path) { + factory JSONPath.fromJson(dynamic value) => JSONPath(value); +} + +@JsonSerializable() +final class SimpleAPIHandler extends APIHandler> { + static const schemaName = 'vyuh.apiContent.handler.simple'; + + final String? title; + final String url; + final Map? headers; + final List<(String name, String value)>? requestBody; + + final JSONPath rootField; + final DisplayFieldMap? fieldMap; + + static final typeDescriptor = TypeDescriptor( + schemaType: SimpleAPIHandler.schemaName, + fromJson: SimpleAPIHandler.fromJson, + title: 'Simple API Handler', + ); + + SimpleAPIHandler({ + this.title, + this.url = '', + this.headers, + this.requestBody, + this.rootField = const JSONPath(r'$'), + this.fieldMap, + }) : super(schemaType: schemaName); + + factory SimpleAPIHandler.fromJson(Map json) => + _$SimpleAPIHandlerFromJson(json); + + @override + Future?> invoke(BuildContext context) async { + final response = await vyuh.network.get(Uri.parse(url)); + final json = jsonDecode(response.body); + + final rootItem = JsonPath(rootField.path).read(json); + if (rootItem.singleOrNull == null) return null; + + final root = rootItem.single.value; + + return switch (root) { + List() => root.map((e) => _createCard(e)).toList(), + Map() => [_createCard(root.values.first)], + _ => null + }; + } + + vf.Card _createCard(Map json) { + final fields = { + 'title': fieldMap?.title, + 'description': fieldMap?.description, + 'imageUrl': fieldMap?.imageUrl, + }.entries.where((x) => x.value != null).map((e) { + final value = + JsonPath(e.value!.path).read(json).singleOrNull?.value as String?; + return MapEntry(e.key, value); + }).fold({}, (previousValue, element) { + previousValue[element.key] = element.value; + return previousValue; + }); + + return vf.Card( + title: fields['title'], + description: fields['description'], + imageUrl: + fields['imageUrl'] != null ? Uri.parse(fields['imageUrl']!) : null, + layout: ListItemCardLayout(title: 'List Item'), + ); + } + + @override + Widget buildData(BuildContext context, List? data) { + return data == null ? empty : _buildCardList(data); + } + + _buildCardList(List data) { + return LimitedBox( + maxHeight: 200, + child: ListView.builder( + itemCount: data.length, + itemBuilder: (context, index) => + vyuh.content.buildContent(context, data[index])), + ); + } +} diff --git a/packages/system/vyuh_feature_system/lib/api_handler/simple_api_handler.g.dart b/packages/system/vyuh_feature_system/lib/api_handler/simple_api_handler.g.dart new file mode 100644 index 00000000..cbf11f4e --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/api_handler/simple_api_handler.g.dart @@ -0,0 +1,47 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'simple_api_handler.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +DisplayFieldMap _$DisplayFieldMapFromJson(Map json) => + DisplayFieldMap( + title: json['title'] == null ? null : JSONPath.fromJson(json['title']), + description: json['description'] == null + ? null + : JSONPath.fromJson(json['description']), + imageUrl: + json['imageUrl'] == null ? null : JSONPath.fromJson(json['imageUrl']), + ); + +SimpleAPIHandler _$SimpleAPIHandlerFromJson(Map json) => + SimpleAPIHandler( + title: json['title'] as String?, + url: json['url'] as String? ?? '', + headers: (json['headers'] as Map?)?.map( + (k, e) => MapEntry(k, e as String), + ), + requestBody: (json['requestBody'] as List?) + ?.map((e) => _$recordConvert( + e, + ($jsonValue) => ( + $jsonValue[r'$1'] as String, + $jsonValue[r'$2'] as String, + ), + )) + .toList(), + rootField: json['rootField'] == null + ? const JSONPath(r'$') + : JSONPath.fromJson(json['rootField']), + fieldMap: json['fieldMap'] == null + ? null + : DisplayFieldMap.fromJson(json['fieldMap'] as Map), + ); + +$Rec _$recordConvert<$Rec>( + Object? value, + $Rec Function(Map) convert, +) => + convert(value as Map); diff --git a/packages/system/vyuh_feature_system/lib/content/api_content.dart b/packages/system/vyuh_feature_system/lib/content/api_content.dart new file mode 100644 index 00000000..1d895ee8 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/api_content.dart @@ -0,0 +1,123 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:vyuh_core/vyuh_core.dart'; +import 'package:vyuh_extension_content/vyuh_extension_content.dart'; +import 'package:vyuh_feature_system/content/empty.dart'; + +part 'api_content.g.dart'; + +@JsonSerializable() +final class APIContent extends ContentItem { + static const schemaName = 'vyuh.apiContent'; + + final bool showPending; + final bool showError; + + @JsonKey(fromJson: typeFromFirstOfListJson) + final APIHandler? handler; + + factory APIContent.fromJson(Map json) => + _$APIContentFromJson(json); + + APIContent({ + this.showError = true, + this.showPending = true, + this.handler, + }) : super(schemaType: APIContent.schemaName); +} + +abstract base class APIHandler { + final String schemaType; + + APIHandler({required this.schemaType}); + + Future invoke(BuildContext context); + + Widget buildData(BuildContext context, T? data); +} + +class APIContentDescriptor extends ContentDescriptor { + final List>? handlers; + + APIContentDescriptor({this.handlers}) + : super(schemaType: APIContent.schemaName, title: 'API Content'); +} + +final class APIContentBuilder extends ContentBuilder { + APIContentBuilder() + : super( + content: TypeDescriptor( + schemaType: APIContent.schemaName, + title: 'API Content', + fromJson: APIContent.fromJson, + ), + defaultLayout: DefaultAPIContentLayout(), + defaultLayoutDescriptor: DefaultAPIContentLayout.typeDescriptor, + ); + + @override + void init(List descriptors) { + super.init(descriptors); + + final apiHandlers = descriptors.cast().expand( + (element) => element.handlers ?? >[]); + + for (final handler in apiHandlers) { + vyuh.content.register(handler); + } + } +} + +final class DefaultAPIContentLayout extends LayoutConfiguration { + static const schemaName = '${APIContent.schemaName}.layout.default'; + static final typeDescriptor = TypeDescriptor( + schemaType: schemaName, + title: 'Default APIContent Layout', + fromJson: DefaultAPIContentLayout.fromJson, + ); + + DefaultAPIContentLayout() : super(schemaType: schemaName); + + factory DefaultAPIContentLayout.fromJson(Map json) => + DefaultAPIContentLayout(); + + @override + Widget build(BuildContext context, APIContent content) { + return FutureBuilder( + future: content.handler?.invoke(context), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (content.showPending && + (snapshot.connectionState == ConnectionState.waiting || + snapshot.connectionState == ConnectionState.active)) { + // Showing a loading spinner during API call + return vyuh.widgetBuilder.contentLoader(); + } else if (snapshot.connectionState == ConnectionState.done) { + if (content.showError && snapshot.hasError) { + // Show error if API call resulted in an error + return vyuh.widgetBuilder.errorView( + title: 'API Error', + subtitle: + 'Handler in context was "${content.handler?.schemaType}"', + error: snapshot.error, + showRestart: false, + ); + } else { + // Show data when API call is successful + return content.handler?.buildData(context, snapshot.data) ?? empty; + } + } else { + // In case, the future is neither in progress nor done. + return kDebugMode + ? vyuh.widgetBuilder.errorView( + title: 'API Error', + error: + 'Something went wrong invoking the api with ${content.handler?.schemaType}', + showRestart: false, + ) + : empty; + } + }, + ); + } +} diff --git a/packages/system/vyuh_feature_system/lib/content/api_content.g.dart b/packages/system/vyuh_feature_system/lib/content/api_content.g.dart new file mode 100644 index 00000000..52316967 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/api_content.g.dart @@ -0,0 +1,13 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'api_content.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +APIContent _$APIContentFromJson(Map json) => APIContent( + showError: json['showError'] as bool? ?? true, + showPending: json['showPending'] as bool? ?? true, + handler: typeFromFirstOfListJson(json['handler']), + ); diff --git a/packages/system/vyuh_feature_system/lib/content/card/card.dart b/packages/system/vyuh_feature_system/lib/content/card/card.dart index 479f8bbf..c11c3d9d 100644 --- a/packages/system/vyuh_feature_system/lib/content/card/card.dart +++ b/packages/system/vyuh_feature_system/lib/content/card/card.dart @@ -19,6 +19,7 @@ class Card extends ContentItem implements PortableBlockItem { final String? title; final String? description; final ImageReference? image; + final Uri? imageUrl; final PortableTextContent? content; final vx.Action? action; @@ -30,6 +31,7 @@ class Card extends ContentItem implements PortableBlockItem { required this.description, this.content, this.image, + this.imageUrl, this.action, this.secondaryAction, this.tertiaryAction, diff --git a/packages/system/vyuh_feature_system/lib/content/card/card.g.dart b/packages/system/vyuh_feature_system/lib/content/card/card.g.dart index 6f8c8817..032f2f06 100644 --- a/packages/system/vyuh_feature_system/lib/content/card/card.g.dart +++ b/packages/system/vyuh_feature_system/lib/content/card/card.g.dart @@ -16,6 +16,9 @@ Card _$CardFromJson(Map json) => Card( image: json['image'] == null ? null : ImageReference.fromJson(json['image'] as Map), + imageUrl: json['imageUrl'] == null + ? null + : Uri.parse(json['imageUrl'] as String), action: json['action'] == null ? null : Action.fromJson(json['action'] as Map), diff --git a/packages/system/vyuh_feature_system/lib/content/card/default_layout.dart b/packages/system/vyuh_feature_system/lib/content/card/default_layout.dart index 17a617ac..73e45bbd 100644 --- a/packages/system/vyuh_feature_system/lib/content/card/default_layout.dart +++ b/packages/system/vyuh_feature_system/lib/content/card/default_layout.dart @@ -41,9 +41,10 @@ class DefaultCardLayout extends LayoutConfiguration { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (content.image?.asset?.ref != null) + if (content.image?.asset?.ref != null || content.imageUrl != null) Flexible( child: e.ContentImage( + url: content.imageUrl?.toString(), ref: content.image?.asset?.ref, fit: BoxFit.contain, ), diff --git a/packages/system/vyuh_feature_system/lib/content/card/list_item_layout.dart b/packages/system/vyuh_feature_system/lib/content/card/list_item_layout.dart index c0ec590f..a728f488 100644 --- a/packages/system/vyuh_feature_system/lib/content/card/list_item_layout.dart +++ b/packages/system/vyuh_feature_system/lib/content/card/list_item_layout.dart @@ -26,42 +26,45 @@ class ListItemCardLayout extends LayoutConfiguration { child: Card( child: Padding( padding: const EdgeInsets.all(8.0), - child: Column( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - children: [ - if (content.image?.asset?.ref != null) - Container( - clipBehavior: Clip.antiAlias, - decoration: - BoxDecoration(borderRadius: BorderRadius.circular(8)), - height: 64, - width: 128, - child: sys.ContentImage( - ref: content.image?.asset?.ref, - fit: BoxFit.contain, + if (content.image?.asset?.ref != null || content.imageUrl != null) + Container( + clipBehavior: Clip.antiAlias, + decoration: + BoxDecoration(borderRadius: BorderRadius.circular(8)), + height: 64, + width: 92, + child: sys.ContentImage( + url: content.imageUrl?.toString(), + ref: content.image?.asset?.ref, + fit: BoxFit.fitWidth, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (content.title != null) + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + content.title!, + style: theme.textTheme.bodyLarge, + ), ), - ), - if (content.title != null) - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Text( - content.title!, - style: theme.textTheme.bodyLarge, + if (content.description != null) + Text( + content.description!, + style: theme.textTheme.bodyMedium, ), - )), - const Icon(Icons.chevron_right_rounded) - ], - ), - if (content.description != null) - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: Text( - content.description!, - style: theme.textTheme.bodyMedium, - ), + ], ), + ), + if (content.action != null) + const Icon(Icons.chevron_right_rounded) ], ), ), diff --git a/packages/system/vyuh_feature_system/lib/content/index.dart b/packages/system/vyuh_feature_system/lib/content/index.dart index 9d09a78c..262787cb 100644 --- a/packages/system/vyuh_feature_system/lib/content/index.dart +++ b/packages/system/vyuh_feature_system/lib/content/index.dart @@ -1,4 +1,5 @@ export 'accordion.dart'; +export 'api_content.dart'; export 'card/card.dart'; export 'conditional.dart'; export 'divider.dart'; diff --git a/packages/system/vyuh_feature_system/lib/feature.dart b/packages/system/vyuh_feature_system/lib/feature.dart index 73833aa4..c40d0a37 100644 --- a/packages/system/vyuh_feature_system/lib/feature.dart +++ b/packages/system/vyuh_feature_system/lib/feature.dart @@ -26,12 +26,11 @@ final feature = FeatureDescriptor( path: '/__system_error__', pageBuilder: (context, state) { return MaterialPage( - child: vyuh.widgetBuilder.errorView( + child: vyuh.widgetBuilder.routeErrorView( title: 'System error', error: state.extra.toString(), onRetry: () => vyuh.tracker.init(), retryLabel: 'Restart', - showRestart: false, ), ); }, @@ -156,7 +155,9 @@ final feature = FeatureDescriptor( ], ), DividerDescriptor(), - AccordionDescriptor() + APIContentDescriptor( + handlers: [SimpleAPIHandler.typeDescriptor], + ), ], contentBuilders: [ RouteContentBuilder(), @@ -169,6 +170,7 @@ final feature = FeatureDescriptor( PortableTextContentBuilder(), DividerContentBuilder(), AccordionContentBuilder(), + APIContentBuilder(), ], conditions: [ TypeDescriptor( diff --git a/packages/system/vyuh_feature_system/lib/vyuh_feature_system.dart b/packages/system/vyuh_feature_system/lib/vyuh_feature_system.dart index ce442e46..d1c0a8b5 100644 --- a/packages/system/vyuh_feature_system/lib/vyuh_feature_system.dart +++ b/packages/system/vyuh_feature_system/lib/vyuh_feature_system.dart @@ -2,6 +2,7 @@ library vyuh_feature_system; export 'action/conditional_action.dart'; export 'action/navigation.dart'; +export 'api_handler/simple_api_handler.dart'; export 'condition/boolean.dart'; export 'content/index.dart'; export 'feature.dart'; diff --git a/packages/system/vyuh_feature_system/pubspec.yaml b/packages/system/vyuh_feature_system/pubspec.yaml index 18c70ddc..6d2b2cef 100644 --- a/packages/system/vyuh_feature_system/pubspec.yaml +++ b/packages/system/vyuh_feature_system/pubspec.yaml @@ -26,6 +26,7 @@ dependencies: vyuh_core: ^1.0.0-beta.1 vyuh_extension_content: ^1.0.0-beta.1 vyuh_extension_script: ^1.0.0-beta.1 + json_path: ^0.7.1 dev_dependencies: flutter_test: