From dcef492de13d22de3d5101e9a510670be0b4dcd4 Mon Sep 17 00:00:00 2001 From: Till Gerken Date: Wed, 25 Jun 2025 15:56:13 +0200 Subject: [PATCH 1/9] Update packages --- macos/Runner.xcodeproj/project.pbxproj | 4 -- pubspec.lock | 88 ++++++++++++++------------ pubspec.yaml | 2 +- 3 files changed, 49 insertions(+), 45 deletions(-) diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 39ea4a5..edc88c4 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -411,14 +411,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; diff --git a/pubspec.lock b/pubspec.lock index 46a37ea..8393ae0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -34,10 +34,10 @@ packages: dependency: transitive description: name: archive - sha256: "0c64e928dcbefddecd234205422bcfc2b5e6d31be0b86fef0d0dd48d7b4c9742" + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" url: "https://pub.dev" source: hosted - version: "4.0.4" + version: "4.0.7" args: dependency: "direct main" description: @@ -154,18 +154,18 @@ packages: dependency: transitive description: name: built_value - sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4 + sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27" url: "https://pub.dev" source: hosted - version: "8.9.5" + version: "8.10.1" chalkdart: dependency: transitive description: name: chalkdart - sha256: e7cfcc9a9d9546843304c1ff87fe0696c7eb82ee70e6df63f555f321b15a40d8 + sha256: "82dfa884e3cf97641eb0742a3b9ffd41490666b9ece548b2e32cbfefe540bf86" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.0" characters: dependency: transitive description: @@ -190,6 +190,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" cli_util: dependency: transitive description: @@ -234,10 +242,10 @@ packages: dependency: transitive description: name: coverage - sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43 + sha256: aa07dbe5f2294c827b7edb9a87bba44a9c15a3cc81bc8da2ca19b37322d30080 url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.14.1" cross_file: dependency: transitive description: @@ -368,10 +376,10 @@ packages: dependency: "direct main" description: name: flutter_bloc - sha256: "1046d719fbdf230330d3443187cc33cc11963d15c9089f6cc56faa42a4c5f0cc" + sha256: cf51747952201a455a1c840f8171d273be009b932c75093020f9af64f2123e38 url: "https://pub.dev" source: hosted - version: "9.1.0" + version: "9.1.1" flutter_code_editor: dependency: "direct main" description: @@ -535,10 +543,10 @@ packages: dependency: transitive description: name: http - sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" http_multi_server: dependency: transitive description: @@ -559,10 +567,10 @@ packages: dependency: transitive description: name: image - sha256: "13d3349ace88f12f4a0d175eb5c12dcdd39d35c4c109a8a13dfeb6d0bd9e31c3" + sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" url: "https://pub.dev" source: hosted - version: "4.5.3" + version: "4.5.4" infinite_scroll_pagination: dependency: "direct main" description: @@ -775,10 +783,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12" + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 url: "https://pub.dev" source: hosted - version: "2.2.16" + version: "2.2.17" path_provider_foundation: dependency: transitive description: @@ -847,18 +855,18 @@ packages: dependency: transitive description: name: posix - sha256: a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a + sha256: f0d7856b6ca1887cfa6d1d394056a296ae33489db914e365e2044fdada449e62 url: "https://pub.dev" source: hosted - version: "6.0.1" + version: "6.0.2" provider: dependency: transitive description: name: provider - sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.5" pub_semver: dependency: "direct main" description: @@ -919,10 +927,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "3ec7210872c4ba945e3244982918e502fa2bfb5230dff6832459ca0e1879b7ad" + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.10" shared_preferences_foundation: dependency: transitive description: @@ -1124,10 +1132,10 @@ packages: dependency: transitive description: name: talker_logger - sha256: ed9b20b8c09efff9f6b7c63fc6630ee2f84aa92661ae09e5ba04e77272bf2ad2 + sha256: f1755d517e5ca8b119b65ad2fc1079746a8d03bd565e75d6b9d5aedf5c1d5b15 url: "https://pub.dev" source: hosted - version: "4.7.1" + version: "4.9.1" term_glyph: dependency: transitive description: @@ -1196,18 +1204,18 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "1d0eae19bd7606ef60fe69ef3b312a437a16549476c42321d5dc1506c9ca3bf4" + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" url: "https://pub.dev" source: hosted - version: "6.3.15" + version: "6.3.16" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" url: "https://pub.dev" source: hosted - version: "6.3.2" + version: "6.3.3" url_launcher_linux: dependency: transitive description: @@ -1236,10 +1244,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" url_launcher_windows: dependency: transitive description: @@ -1276,10 +1284,10 @@ packages: dependency: transitive description: name: watcher - sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" + sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" web: dependency: transitive description: @@ -1292,18 +1300,18 @@ packages: dependency: transitive description: name: web_socket - sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" url: "https://pub.dev" source: hosted - version: "0.1.6" + version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" webkit_inspection_protocol: dependency: transitive description: @@ -1316,10 +1324,10 @@ packages: dependency: transitive description: name: win32 - sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f + sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba" url: "https://pub.dev" source: hosted - version: "5.12.0" + version: "5.13.0" xdg_directories: dependency: transitive description: @@ -1340,10 +1348,10 @@ packages: dependency: transitive description: name: xml2json - sha256: "9ff65c144a48f7f73d2bd6d919700c7697beced3b51216b982f78c68bc0da142" + sha256: "8a7ae63b76676f083d81287b61f03222952609c0507893bc62e60a4a588a9702" url: "https://pub.dev" source: hosted - version: "6.2.6" + version: "6.2.7" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 695aa71..e6a77d7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,7 +24,7 @@ dependencies: fhir: 0.12.0 flutter: sdk: flutter - flutter_bloc: 9.1.0 + flutter_bloc: 9.1.1 flutter_code_editor: 0.3.3 flutter_highlight: 0.7.0 flutter_localizations: From 2ee24d17604b923780a56090a604ca41ed1d9fff Mon Sep 17 00:00:00 2001 From: Till Gerken Date: Wed, 25 Jun 2025 16:57:06 +0200 Subject: [PATCH 2/9] Migrate to updated packages --- lib/app.dart | 49 ++--- lib/app_drawer_menu.dart | 53 ++---- lib/dashboard/dashboard_route.dart | 16 -- .../widgets/resource_paginated_list.dart | 180 +++++++++--------- lib/license/license_routes.dart | 36 ---- .../pages/third_party_licenses_page.dart | 1 - lib/routes.dart | 51 ++++- lib/routes.g.dart | 33 +++- pubspec.lock | 64 ++++--- pubspec.yaml | 23 +-- 10 files changed, 240 insertions(+), 266 deletions(-) delete mode 100644 lib/dashboard/dashboard_route.dart delete mode 100644 lib/license/license_routes.dart diff --git a/lib/app.dart b/lib/app.dart index ab7d8a2..06109f2 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,6 +1,5 @@ import 'package:fire_scribe/app_scaffold.dart'; import 'package:fire_scribe/app_theme.dart'; -import 'package:fire_scribe/dashboard/dashboard_route.dart'; import 'package:fire_scribe/l10n/app_localizations.dart'; import 'package:fire_scribe/l10n/cubit/localization_cubit.dart'; import 'package:fire_scribe/l10n/supported_locales.dart'; @@ -12,9 +11,7 @@ import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; class App extends StatelessWidget { - const App({ - super.key, - }); + const App({super.key}); @override Widget build(BuildContext context) { @@ -24,35 +21,33 @@ class App extends StatelessWidget { navigatorKey: GetIt.instance>(), routes: $appRoutes, errorBuilder: (context, state) { - return AppScaffold( - child: _AppErrorPage(), - ); + return AppScaffold(child: _AppErrorPage()); }, ); return BlocProvider( - create: (final context) => LocalizationCubit( - sharedPreferences: GetIt.instance(), - ), + create: + (final context) => + LocalizationCubit(sharedPreferences: GetIt.instance()), child: BlocBuilder( - buildWhen: (final _, final current) => current.when( - initial: () => false, - locale: (final _) => true, - ), + buildWhen: + (final _, final current) => + current.when(initial: () => false, locale: (final _) => true), builder: (final context, final state) { return state.when( initial: () => const SizedBox(), - locale: (final locale) => MaterialApp.router( - title: S.of(context).appTitle, - theme: appTheme, - debugShowCheckedModeBanner: false, - supportedLocales: supportedLocales, - locale: locale, - localizationsDelegates: S.delegates, - routeInformationProvider: router.routeInformationProvider, - routeInformationParser: router.routeInformationParser, - routerDelegate: router.routerDelegate, - ), + locale: + (final locale) => MaterialApp.router( + title: S.of(context).appTitle, + theme: appTheme, + debugShowCheckedModeBanner: false, + supportedLocales: supportedLocales, + locale: locale, + localizationsDelegates: S.delegates, + routeInformationProvider: router.routeInformationProvider, + routeInformationParser: router.routeInformationParser, + routerDelegate: router.routerDelegate, + ), ); }, ), @@ -80,9 +75,7 @@ class _AppErrorPage extends StatelessWidget { SizedBox(height: 10), ElevatedButton( onPressed: () => GoRouter.of(context).refresh(), - child: Text( - S.of(context).refresh, - ), + child: Text(S.of(context).refresh), ), ], ), diff --git a/lib/app_drawer_menu.dart b/lib/app_drawer_menu.dart index ce2f07f..e82030e 100644 --- a/lib/app_drawer_menu.dart +++ b/lib/app_drawer_menu.dart @@ -1,6 +1,4 @@ -import 'package:fire_scribe/dashboard/dashboard_route.dart'; import 'package:fire_scribe/l10n/app_localizations.dart'; -import 'package:fire_scribe/license/license_routes.dart'; import 'package:fire_scribe/routes.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -14,18 +12,14 @@ class AppDrawerMenu extends StatelessWidget { elevation: 2, shadowColor: Theme.of(context).colorScheme.shadow, child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 22, - ), + padding: const EdgeInsets.symmetric(vertical: 22), child: ListView( physics: ClampingScrollPhysics(), children: [ InkWell( onTap: () => DashboardRoute().go(context), child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8, - ), + padding: const EdgeInsets.symmetric(horizontal: 8), child: Image.asset( 'assets/app/logo.png', width: 52, @@ -45,11 +39,7 @@ class AppDrawerMenu extends StatelessWidget { title: S.of(context).thirdPartyLicenses, route: ThirdPartyLicensesRoute().location, ), - ].map( - (item) => AppDrawerMenuItem( - data: item, - ), - ), + ].map((item) => AppDrawerMenuItem(data: item)), ], ), ), @@ -60,10 +50,7 @@ class AppDrawerMenu extends StatelessWidget { class AppDrawerMenuItem extends StatelessWidget { final AppDrawerMenuItemData data; - const AppDrawerMenuItem({ - super.key, - required this.data, - }); + const AppDrawerMenuItem({super.key, required this.data}); @override Widget build(BuildContext context) { final location = GoRouterState.of(context).uri; @@ -78,32 +65,24 @@ class AppDrawerMenuItem extends StatelessWidget { child: Column( children: [ Padding( - padding: EdgeInsets.symmetric( - vertical: 4.0, - ), + padding: EdgeInsets.symmetric(vertical: 4.0), child: Container( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), - decoration: isSelected - ? BoxDecoration( - color: Theme.of(context) - .colorScheme - .primaryContainer - .withValues(alpha: 0.3), - borderRadius: BorderRadius.circular(24), - ) - : null, - child: Icon( - data.icon, - fill: 1, - size: 24, - ), + decoration: + isSelected + ? BoxDecoration( + color: Theme.of( + context, + ).colorScheme.primaryContainer.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(24), + ) + : null, + child: Icon(data.icon, fill: 1, size: 24), ), ), SizedBox(width: 4), Padding( - padding: EdgeInsets.symmetric( - horizontal: 8.0, - ), + padding: EdgeInsets.symmetric(horizontal: 8.0), child: Text( data.title, textAlign: TextAlign.center, diff --git a/lib/dashboard/dashboard_route.dart b/lib/dashboard/dashboard_route.dart deleted file mode 100644 index ad6423c..0000000 --- a/lib/dashboard/dashboard_route.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:fire_scribe/app_scaffold.dart'; -import 'package:fire_scribe/dashboard/dashboard_page.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; - -@immutable -class DashboardRoute extends GoRouteData { - @override - Page buildPage(BuildContext context, GoRouterState state) { - return NoTransitionPage( - child: AppScaffold( - child: DashboardPage(), - ), - ); - } -} diff --git a/lib/dashboard/widgets/resource_paginated_list.dart b/lib/dashboard/widgets/resource_paginated_list.dart index 39dfccf..37158f2 100644 --- a/lib/dashboard/widgets/resource_paginated_list.dart +++ b/lib/dashboard/widgets/resource_paginated_list.dart @@ -13,50 +13,44 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:intl/intl.dart'; class ResourcePaginatedList extends StatefulWidget { - const ResourcePaginatedList({ - super.key, - }); + const ResourcePaginatedList({super.key}); @override State createState() => _ResourcePaginatedListState(); } class _ResourcePaginatedListState extends State { - final pagingController = PagingController(firstPageKey: 0); + // final pagingController = PagingController(firstPageKey: 0); + late final pagingController = PagingController( + fetchPage: (pageKey) => fetchPage(pageKey), + getNextPageKey: + (state) => state.lastPageIsEmpty ? null : state.nextIntPageKey, + ); + String? entitySelected; String? searchCursor; - @override - void initState() { - super.initState(); - pagingController.addPageRequestListener(fetchPage); - } - @override void dispose() { - pagingController.removePageRequestListener(fetchPage); pagingController.dispose(); super.dispose(); } - Future fetchPage(final int pageOffset) async { + Future> fetchPage(final int pageOffset) async { final currenEntity = entitySelected; if (currenEntity == null || currenEntity.isEmpty) { - pagingController.appendLastPage([]); - return; + return []; } - final parameters = { - '_sort': '-_lastUpdated', - '_count': '25', - }; + final parameters = {'_sort': '-_lastUpdated', '_count': '25'}; if (searchCursor != null) { parameters['ct'] = searchCursor!; } - final rawBundle = - await BlocProvider.of(context).request( + final rawBundle = await BlocProvider.of( + context, + ).request( request: FhirRequest( operation: FhirRequestOperation.search, entityName: currenEntity, @@ -64,36 +58,26 @@ class _ResourcePaginatedListState extends State { ), ); if (rawBundle == null) { - pagingController.appendLastPage([]); - return; + return []; } final bundle = Bundle.fromJson(rawBundle); final nextUrl = bundle.link?.firstWhereOrNull((item) => item.relation == 'next')?.url; searchCursor = nextUrl?.value?.queryParameters['ct']; - final entries = (bundle.entry ?? []) - .map((entry) => entry.resource) - .nonNulls - .toList(); + final entries = + (bundle.entry ?? []) + .map((entry) => entry.resource) + .nonNulls + .toList(); - if (searchCursor == null) { - pagingController.appendLastPage(entries); - } else { - pagingController.appendPage( - entries, - (pagingController.itemList?.length ?? 0) + entries.length, - ); - } + return entries; } void updateExistingResource(final int index, final Resource resource) { setState(() { - pagingController.itemList?.removeAt(index); - pagingController.itemList?.insert( - 0, - resource, - ); + pagingController.items?.removeAt(index); + pagingController.items?.insert(0, resource); }); } @@ -101,17 +85,18 @@ class _ResourcePaginatedListState extends State { if (entitySelected == null) { return; } - final resource = await FhirResourceEditorBottomSheet.show(context, - resource: ResourceX.factoryCreation(entitySelected!)); + final resource = await FhirResourceEditorBottomSheet.show( + context, + resource: ResourceX.factoryCreation(entitySelected!), + ); if (resource != null) { context.popAndPushSnackbar( - message: S.of(context).resourceCreationSuccessful( - entitySelected!, - resource.fhirId ?? '', - ), + message: S + .of(context) + .resourceCreationSuccessful(entitySelected!, resource.fhirId ?? ''), ); setState(() { - pagingController.itemList?.insert(0, resource); + pagingController.items?.insert(0, resource); }); } } @@ -127,7 +112,7 @@ class _ResourcePaginatedListState extends State { ), ); setState(() { - pagingController.itemList?.removeAt(index); + pagingController.items?.removeAt(index); }); } catch (e) { context.popAndPushSnackbar( @@ -152,12 +137,13 @@ class _ResourcePaginatedListState extends State { }, builder: (context, state) { return state.when( - noselected: () => Center( - child: Text( - S.of(context).selectAnEntityFromList, - style: Theme.of(context).textTheme.labelLarge, - ), - ), + noselected: + () => Center( + child: Text( + S.of(context).selectAnEntityFromList, + style: Theme.of(context).textTheme.labelLarge, + ), + ), selected: (entitySelected) { return Column( children: [ @@ -186,40 +172,50 @@ class _ResourcePaginatedListState extends State { ), ), Expanded( - child: PagedListView.separated( - pagingController: pagingController, - physics: const AlwaysScrollableScrollPhysics(), - padding: const EdgeInsets.symmetric(horizontal: 24), - itemExtent: 80, - builderDelegate: PagedChildBuilderDelegate( - newPageProgressIndicatorBuilder: (context) => - const Center( - child: CircularProgressIndicator(), - ), - noItemsFoundIndicatorBuilder: (context) => Center( - child: Text( - S.of(context).noDataAssociatedToAnEntity, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyLarge, - ), - ), - noMoreItemsIndicatorBuilder: (context) => - const SizedBox(), - itemBuilder: (context, item, index) { - return ResourcePaginatedListCard( - resource: item, - resourceWasUpdated: (resource) => - updateExistingResource( - index, - resource, + child: PagingListener( + controller: pagingController, + builder: + ( + context, + state, + fetchNextPage, + ) => PagedListView.separated( + state: state, + fetchNextPage: fetchNextPage, + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 24), + itemExtent: 80, + builderDelegate: PagedChildBuilderDelegate( + newPageProgressIndicatorBuilder: + (context) => const Center( + child: CircularProgressIndicator(), + ), + noItemsFoundIndicatorBuilder: + (context) => Center( + child: Text( + S.of(context).noDataAssociatedToAnEntity, + textAlign: TextAlign.center, + style: + Theme.of(context).textTheme.bodyLarge, + ), + ), + noMoreItemsIndicatorBuilder: + (context) => const SizedBox(), + itemBuilder: (context, item, index) { + return ResourcePaginatedListCard( + resource: item, + resourceWasUpdated: + (resource) => + updateExistingResource(index, resource), + resourceDelete: + (resource) => + deleteResource(index, resource), + ); + }, ), - resourceDelete: (resource) => - deleteResource(index, resource), - ); - }, - ), - separatorBuilder: (context, index) => - const SizedBox(height: 2), + separatorBuilder: + (context, index) => const SizedBox(height: 2), + ), ), ), ], @@ -234,10 +230,7 @@ class _ResourcePaginatedListState extends State { class ResourcePaginatedListHeader extends StatelessWidget { final String entityType; - const ResourcePaginatedListHeader({ - super.key, - required this.entityType, - }); + const ResourcePaginatedListHeader({super.key, required this.entityType}); @override Widget build(BuildContext context) { return Row( @@ -288,10 +281,7 @@ class ResourcePaginatedListCard extends StatelessWidget { return InkWell( onTap: () => _showCodeEditor(context), child: Container( - padding: EdgeInsets.symmetric( - horizontal: 24, - vertical: 12, - ), + padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainerLowest, borderRadius: BorderRadius.circular(12), @@ -312,7 +302,7 @@ class ResourcePaginatedListCard extends StatelessWidget { IconButton( onPressed: () => {resourceDelete(resource)}, icon: const Icon(Icons.delete), - ) + ), ], ), ), diff --git a/lib/license/license_routes.dart b/lib/license/license_routes.dart deleted file mode 100644 index d65359c..0000000 --- a/lib/license/license_routes.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:fire_scribe/app_scaffold.dart'; -import 'package:fire_scribe/license/pages/mic_oss_license_single_page.dart'; -import 'package:fire_scribe/license/pages/third_party_licenses_page.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; - -@immutable -class ThirdPartyLicensesRoute extends GoRouteData { - @override - Page buildPage(final BuildContext context, final GoRouterState state) { - return NoTransitionPage( - child: AppScaffold( - child: ThirdPartyLicensesPage(), - ), - ); - } -} - -@immutable -class ThirdPartyLicensesDetailRoute extends GoRouteData { - final String thirdPartyLicensePackageName; - const ThirdPartyLicensesDetailRoute({ - required this.thirdPartyLicensePackageName, - }); - - @override - Page buildPage(final BuildContext context, final GoRouterState state) { - return NoTransitionPage( - child: AppScaffold( - child: MiscOssLicenseSinglePage( - packageName: thirdPartyLicensePackageName, - ), - ), - ); - } -} diff --git a/lib/license/pages/third_party_licenses_page.dart b/lib/license/pages/third_party_licenses_page.dart index ac5063c..4d2d842 100644 --- a/lib/license/pages/third_party_licenses_page.dart +++ b/lib/license/pages/third_party_licenses_page.dart @@ -1,6 +1,5 @@ import 'package:collection/collection.dart'; import 'package:fire_scribe/l10n/app_localizations.dart'; -import 'package:fire_scribe/license/license_routes.dart'; import 'package:fire_scribe/oss_licenses.dart'; import 'package:fire_scribe/routes.dart'; import 'package:flutter/foundation.dart'; diff --git a/lib/routes.dart b/lib/routes.dart index cc473a1..a8c0f4e 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -1,5 +1,7 @@ -import 'package:fire_scribe/dashboard/dashboard_route.dart'; -import 'package:fire_scribe/license/license_routes.dart'; +import 'package:fire_scribe/app_scaffold.dart'; +import 'package:fire_scribe/dashboard/dashboard_page.dart'; +import 'package:fire_scribe/license/pages/mic_oss_license_single_page.dart'; +import 'package:fire_scribe/license/pages/third_party_licenses_page.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -7,15 +9,13 @@ part 'routes.g.dart'; @TypedShellRoute( routes: [ - TypedGoRoute( - path: '/dashboard', - ), + TypedGoRoute(path: '/dashboard'), TypedGoRoute( path: '/third-party-licenses', routes: [ TypedGoRoute( path: ':thirdPartyLicensePackageName', - ) + ), ], ), ], @@ -27,3 +27,42 @@ class HomeRoute extends ShellRouteData { return navigator; } } + +@immutable +class DashboardRoute extends GoRouteData with _$DashboardRoute { + @override + Page buildPage(BuildContext context, GoRouterState state) { + return NoTransitionPage(child: AppScaffold(child: DashboardPage())); + } +} + +@immutable +class ThirdPartyLicensesRoute extends GoRouteData + with _$ThirdPartyLicensesRoute { + @override + Page buildPage(final BuildContext context, final GoRouterState state) { + return NoTransitionPage( + child: AppScaffold(child: ThirdPartyLicensesPage()), + ); + } +} + +@immutable +class ThirdPartyLicensesDetailRoute extends GoRouteData + with _$ThirdPartyLicensesDetailRoute { + final String thirdPartyLicensePackageName; + const ThirdPartyLicensesDetailRoute({ + required this.thirdPartyLicensePackageName, + }); + + @override + Page buildPage(final BuildContext context, final GoRouterState state) { + return NoTransitionPage( + child: AppScaffold( + child: MiscOssLicenseSinglePage( + packageName: thirdPartyLicensePackageName, + ), + ), + ); + } +} diff --git a/lib/routes.g.dart b/lib/routes.g.dart index 356e24b..1fe85d4 100644 --- a/lib/routes.g.dart +++ b/lib/routes.g.dart @@ -15,15 +15,15 @@ RouteBase get $homeRoute => ShellRouteData.$route( routes: [ GoRouteData.$route( path: '/dashboard', - factory: $DashboardRouteExtension._fromState, + factory: _$DashboardRoute._fromState, ), GoRouteData.$route( path: '/third-party-licenses', - factory: $ThirdPartyLicensesRouteExtension._fromState, + factory: _$ThirdPartyLicensesRoute._fromState, routes: [ GoRouteData.$route( path: ':thirdPartyLicensePackageName', - factory: $ThirdPartyLicensesDetailRouteExtension._fromState, + factory: _$ThirdPartyLicensesDetailRoute._fromState, ), ], ), @@ -34,59 +34,76 @@ extension $HomeRouteExtension on HomeRoute { static HomeRoute _fromState(GoRouterState state) => HomeRoute(); } -extension $DashboardRouteExtension on DashboardRoute { +mixin _$DashboardRoute on GoRouteData { static DashboardRoute _fromState(GoRouterState state) => DashboardRoute(); + @override String get location => GoRouteData.$location( '/dashboard', ); + @override void go(BuildContext context) => context.go(location); + @override Future push(BuildContext context) => context.push(location); + @override void pushReplacement(BuildContext context) => context.pushReplacement(location); + @override void replace(BuildContext context) => context.replace(location); } -extension $ThirdPartyLicensesRouteExtension on ThirdPartyLicensesRoute { +mixin _$ThirdPartyLicensesRoute on GoRouteData { static ThirdPartyLicensesRoute _fromState(GoRouterState state) => ThirdPartyLicensesRoute(); + @override String get location => GoRouteData.$location( '/third-party-licenses', ); + @override void go(BuildContext context) => context.go(location); + @override Future push(BuildContext context) => context.push(location); + @override void pushReplacement(BuildContext context) => context.pushReplacement(location); + @override void replace(BuildContext context) => context.replace(location); } -extension $ThirdPartyLicensesDetailRouteExtension - on ThirdPartyLicensesDetailRoute { +mixin _$ThirdPartyLicensesDetailRoute on GoRouteData { static ThirdPartyLicensesDetailRoute _fromState(GoRouterState state) => ThirdPartyLicensesDetailRoute( thirdPartyLicensePackageName: state.pathParameters['thirdPartyLicensePackageName']!, ); + ThirdPartyLicensesDetailRoute get _self => + this as ThirdPartyLicensesDetailRoute; + + @override String get location => GoRouteData.$location( - '/third-party-licenses/${Uri.encodeComponent(thirdPartyLicensePackageName)}', + '/third-party-licenses/${Uri.encodeComponent(_self.thirdPartyLicensePackageName)}', ); + @override void go(BuildContext context) => context.go(location); + @override Future push(BuildContext context) => context.push(location); + @override void pushReplacement(BuildContext context) => context.pushReplacement(location); + @override void replace(BuildContext context) => context.replace(location); } diff --git a/pubspec.lock b/pubspec.lock index 8393ae0..75897f8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -142,6 +142,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.0.0" + build_verify: + dependency: "direct dev" + description: + name: build_verify + sha256: b136572561f0b70c2e60c2a1afe5104027af533de65508e5f421922a28d8a74a + url: "https://pub.dev" + source: hosted + version: "2.0.0" built_collection: dependency: "direct main" description: @@ -162,10 +170,10 @@ packages: dependency: transitive description: name: chalkdart - sha256: "82dfa884e3cf97641eb0742a3b9ffd41490666b9ece548b2e32cbfefe540bf86" + sha256: "7ffc6bd39c81453fb9ba8dbce042a9c960219b75ea1c07196a7fa41c2fab9e86" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "3.0.5" characters: dependency: transitive description: @@ -400,10 +408,10 @@ packages: dependency: "direct dev" description: name: flutter_launcher_icons - sha256: bfa04787c85d80ecb3f8777bde5fc10c3de809240c48fa061a2c2bf15ea5211c + sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" url: "https://pub.dev" source: hosted - version: "0.14.3" + version: "0.14.4" flutter_lints: dependency: "direct dev" description: @@ -447,10 +455,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e" + sha256: "62b248b2dfb06ded10c84b713215b25aea020a5b08c32e801a974361557ebc3f" url: "https://pub.dev" source: hosted - version: "2.5.7" + version: "3.0.0-0.0.dev" freezed_annotation: dependency: "direct main" description: @@ -487,18 +495,18 @@ packages: dependency: "direct main" description: name: go_router - sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3 + sha256: ac294be30ba841830cfa146e5a3b22bb09f8dc5a0fdd9ca9332b04b0bde99ebf url: "https://pub.dev" source: hosted - version: "14.8.1" + version: "15.2.4" go_router_builder: dependency: "direct dev" description: name: go_router_builder - sha256: a79845a602f9ce3d837408c408915ea034eb2957fe0b527cfa3fc50e2e9bcb93 + sha256: "2357997b3419219ee7096c861e65506475202a49cf38153efab0e82370925eaa" url: "https://pub.dev" source: hosted - version: "2.8.2" + version: "3.0.1" google_fonts: dependency: "direct main" description: @@ -575,10 +583,10 @@ packages: dependency: "direct main" description: name: infinite_scroll_pagination - sha256: "4047eb8191e8b33573690922a9e995af64c3949dc87efc844f936b039ea279df" + sha256: "9b8f95362928a1de835658835194b435c40f2bfc386f6545c1fd706203df0cd4" url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "5.1.0" intl: dependency: "direct main" description: @@ -591,10 +599,10 @@ packages: dependency: "direct dev" description: name: intl_translation - sha256: b858d88b569f3c529e992ba7186aa495f3e862897df60edb932563c619943610 + sha256: b3f1ebfab4109d1a946b45c57523628da92a0e2a2df5f2d9981ef4334fd24a26 url: "https://pub.dev" source: hosted - version: "0.20.0" + version: "0.20.1" io: dependency: transitive description: @@ -695,10 +703,10 @@ packages: dependency: "direct main" description: name: material_symbols_icons - sha256: db745002d0323c32097f5bc23711c02b0fb36ef616011a38c8c1798d5684d368 + sha256: "7c50901b39d1ad645ee25d920aed008061e1fd541a897b4ebf2c01d966dbf16b" url: "https://pub.dev" source: hosted - version: "4.2810.0" + version: "4.2815.1" meta: dependency: transitive description: @@ -903,18 +911,18 @@ packages: dependency: transitive description: name: share_plus - sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da + sha256: b2961506569e28948d75ec346c28775bb111986bb69dc6a20754a457e3d97fa0 url: "https://pub.dev" source: hosted - version: "10.1.4" + version: "11.0.0" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b + sha256: "1032d392bc5d2095a77447a805aa3f804d2ae6a4d5eef5e6ebb3bd94c1bc19ef" url: "https://pub.dev" source: hosted - version: "5.0.2" + version: "6.0.0" shared_preferences: dependency: "direct main" description: @@ -1100,34 +1108,34 @@ packages: dependency: "direct main" description: name: talker - sha256: "45abef5b92f9b9bd42c3f20133ad4b20ab12e1da2aa206fc0a40ea874bed7c5d" + sha256: cf02a0d294701c76022f32bc8eb7e6f943953eb17fa1f8aaee56af210848134b url: "https://pub.dev" source: hosted - version: "4.7.1" + version: "4.9.1" talker_bloc_logger: dependency: "direct main" description: name: talker_bloc_logger - sha256: "2214a5f6ef9ff33494dc6149321c270356962725cc8fc1a485d44b1d9b812ddd" + sha256: fce90b694728bb1e5cba831047eb051b38e99900256a66725b8057b3452e6730 url: "https://pub.dev" source: hosted - version: "4.7.1" + version: "4.9.1" talker_dio_logger: dependency: "direct main" description: name: talker_dio_logger - sha256: "52c1b554cccedec6073637a6d4f6a3e267dd4451c1545fe57e1b26897a560ccb" + sha256: "926688b8ee3d4328d16a978d2d400145afec5448b32014bbb255904bfa648c6e" url: "https://pub.dev" source: hosted - version: "4.7.1" + version: "4.9.1" talker_flutter: dependency: "direct main" description: name: talker_flutter - sha256: "77458ca11638dfefb651e898a26101ee54e60dc0b168ad7481a05b1c97ce2680" + sha256: "9573cca369b964c8a8d34f422b74d4b934ac64341dd31560352fe458ba6b7894" url: "https://pub.dev" source: hosted - version: "4.7.1" + version: "4.9.1" talker_logger: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e6a77d7..f9a0738 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,33 +32,34 @@ dependencies: freezed_annotation: 2.4.4 get_it: 8.0.3 google_fonts: 6.2.1 - go_router: 14.8.1 + go_router: 15.2.4 highlight: 0.7.0 - infinite_scroll_pagination: 4.1.0 + infinite_scroll_pagination: 5.1.0 intl: 0.19.0 - material_symbols_icons: 4.2810.0 + material_symbols_icons: 4.2815.1 package_info_plus: 8.3.0 path: 1.9.1 pub_semver: 2.2.0 shared_preferences: 2.5.3 stack_trace: 1.12.1 - talker: 4.7.1 - talker_bloc_logger: 4.7.1 - talker_dio_logger: 4.7.1 - talker_flutter: 4.7.1 + talker: 4.9.1 + talker_bloc_logger: 4.9.1 + talker_dio_logger: 4.9.1 + talker_flutter: 4.9.1 dev_dependencies: bloc_test: 10.0.0 build_runner: 2.4.15 + build_verify: 2.0.0 dartz: 0.10.1 - flutter_launcher_icons: 0.14.3 + flutter_launcher_icons: 0.14.4 flutter_lints: 5.0.0 flutter_oss_licenses: 3.0.4 flutter_test: sdk: flutter - freezed: 2.5.7 - go_router_builder: 2.8.2 - intl_translation: 0.20.0 + freezed: 3.0.0-0.0.dev + go_router_builder: 3.0.1 + intl_translation: 0.20.1 mocktail: 1.0.4 remove_from_coverage: 2.0.0 From feb000326d0dcaa7383cc03b7d94c754f48d3fb0 Mon Sep 17 00:00:00 2001 From: Till Gerken Date: Wed, 25 Jun 2025 17:07:23 +0200 Subject: [PATCH 3/9] More package upgrades --- devtools_options.yaml | 3 +++ pubspec.lock | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 devtools_options.yaml diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/pubspec.lock b/pubspec.lock index 75897f8..d076a40 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1028,10 +1028,10 @@ packages: dependency: transitive description: name: source_gen - sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "2.0.0" source_helper: dependency: transitive description: From b1da56a13b7b1ed5db0f50fc3d409d47ecf5fc2e Mon Sep 17 00:00:00 2001 From: Till Gerken Date: Wed, 25 Jun 2025 17:24:58 +0200 Subject: [PATCH 4/9] Fix pagination with new go_router API --- .../widgets/resource_paginated_list.dart | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/lib/dashboard/widgets/resource_paginated_list.dart b/lib/dashboard/widgets/resource_paginated_list.dart index 37158f2..f78c197 100644 --- a/lib/dashboard/widgets/resource_paginated_list.dart +++ b/lib/dashboard/widgets/resource_paginated_list.dart @@ -24,7 +24,18 @@ class _ResourcePaginatedListState extends State { late final pagingController = PagingController( fetchPage: (pageKey) => fetchPage(pageKey), getNextPageKey: - (state) => state.lastPageIsEmpty ? null : state.nextIntPageKey, + // The infinite scroller doesn't have appendLastPage anymore, so we + // need to determine when the last page is reached. The logic here is + // to check if the last page was empty (or never returned any results), + // or alternatively see if we already retrieved items (items.isNotEmpty + // is true) and the "next" searchCursor is null. In that case the + // server doesn't offer more data after the last page. + (state) => + (state.lastPageIsEmpty || + ((state.items?.isNotEmpty ?? false) && + searchCursor == null)) + ? null + : state.nextIntPageKey, ); String? entitySelected; @@ -37,8 +48,8 @@ class _ResourcePaginatedListState extends State { } Future> fetchPage(final int pageOffset) async { - final currenEntity = entitySelected; - if (currenEntity == null || currenEntity.isEmpty) { + final currentEntity = entitySelected; + if (currentEntity == null || currentEntity.isEmpty) { return []; } @@ -53,7 +64,7 @@ class _ResourcePaginatedListState extends State { ).request( request: FhirRequest( operation: FhirRequestOperation.search, - entityName: currenEntity, + entityName: currentEntity, parameters: parameters, ), ); From 3e835bb0e8bb7a30d3c49df3addde96c1612928e Mon Sep 17 00:00:00 2001 From: Till Gerken Date: Wed, 25 Jun 2025 17:41:37 +0200 Subject: [PATCH 5/9] Add import/export route and page --- lib/app_drawer_menu.dart | 5 + lib/import_export/import_export_page.dart | 40 +++ lib/l10n/app_localizations.dart | 345 +++++++++++----------- lib/l10n/l10n/intl_messages.arb | 12 + lib/routes.dart | 10 + lib/routes.g.dart | 27 ++ 6 files changed, 273 insertions(+), 166 deletions(-) create mode 100644 lib/import_export/import_export_page.dart diff --git a/lib/app_drawer_menu.dart b/lib/app_drawer_menu.dart index e82030e..b18f58a 100644 --- a/lib/app_drawer_menu.dart +++ b/lib/app_drawer_menu.dart @@ -34,6 +34,11 @@ class AppDrawerMenu extends StatelessWidget { title: S.of(context).server, route: DashboardRoute().location, ), + AppDrawerMenuItemData( + icon: Symbols.import_export, + title: S.of(context).importExport, + route: ImportExportRoute().location, + ), AppDrawerMenuItemData( icon: Symbols.license, title: S.of(context).thirdPartyLicenses, diff --git a/lib/import_export/import_export_page.dart b/lib/import_export/import_export_page.dart new file mode 100644 index 0000000..a31dd62 --- /dev/null +++ b/lib/import_export/import_export_page.dart @@ -0,0 +1,40 @@ +import 'package:fire_scribe/l10n/app_localizations.dart'; +import 'package:flutter/material.dart'; + +/// A page for handling import and export functionality of FHIR resources. +/// +/// This page provides a user interface for importing FHIR resources from external +/// sources and exporting resources to various formats. The content will be +/// implemented in subsequent steps. +class ImportExportPage extends StatelessWidget { + const ImportExportPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.import_export, + size: 64, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(height: 16), + Text( + S.of(context).importExport, + style: Theme.of(context).textTheme.headlineMedium, + ), + const SizedBox(height: 8), + Text( + S.of(context).importExportDescription, + style: Theme.of(context).textTheme.bodyLarge, + textAlign: TextAlign.center, + ), + ], + ), + ), + ); + } +} diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index a989488..1f63332 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -17,9 +17,10 @@ class S { static Future load(final Locale locale) async { // If we're given "en_US", we'll use it as-is. If we're // given "en", we extract it and use it. - final localeName = locale.countryCode == null || locale.countryCode!.isEmpty - ? locale.languageCode - : locale.toString(); + final localeName = + locale.countryCode == null || locale.countryCode!.isEmpty + ? locale.languageCode + : locale.toString(); // We make sure the locale name is in the right format e.g. // converting "en-US" to "en_US". @@ -44,11 +45,11 @@ class S { } static List get delegates => [ - const AppLocalizationsDelegate(), - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ]; + const AppLocalizationsDelegate(), + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ]; static String? getLocalizedString(final BuiltMap data) { // @@ -63,7 +64,8 @@ class S { // As a fallback, retrieve the string by language lookup. // (de, en, it, ...) // - final shortLocale = Intl.defaultLocale?.substring(0, 2) ?? + final shortLocale = + Intl.defaultLocale?.substring(0, 2) ?? supportedLocales.first.languageCode; if (data.keys.contains(shortLocale)) { return data[shortLocale]; @@ -83,134 +85,133 @@ class S { } String get appTitle => Intl.message( - 'Fire Scribe', - name: 'appTitle', - desc: 'The title of the app', - ); + 'Fire Scribe', + name: 'appTitle', + desc: 'The title of the app', + ); - String get appErrorPageMessage => - Intl.message('Internal error, refresh the page or restart the app.', - name: 'appErrorPageMessage', - desc: 'The message shown when a user accesses an incorrect route'); + String get appErrorPageMessage => Intl.message( + 'Internal error, refresh the page or restart the app.', + name: 'appErrorPageMessage', + desc: 'The message shown when a user accesses an incorrect route', + ); String get refresh => Intl.message('Refresh', name: 'refresh', desc: 'Refresh word'); String get allResources => Intl.message( - 'All resources', - name: 'allResources', - desc: 'The title of the entity list section', - ); + 'All resources', + name: 'allResources', + desc: 'The title of the entity list section', + ); String get serverUrl => Intl.message( - 'Server URL', - name: 'serverUrl', - desc: 'The label of the server connection text field', - ); + 'Server URL', + name: 'serverUrl', + desc: 'The label of the server connection text field', + ); String get introduceServerUrl => Intl.message( - 'Enter Server URL', - name: 'introduceServerUrl', - desc: 'The hint text of the server connection text field', - ); + 'Enter Server URL', + name: 'introduceServerUrl', + desc: 'The hint text of the server connection text field', + ); String get serverUrlFormEmpty => Intl.message( - 'Enter the URL of the server to connect before proceeding.', - name: 'serverUrlFormEmpty', - desc: 'The error message when user connect with an empty server url', - ); + 'Enter the URL of the server to connect before proceeding.', + name: 'serverUrlFormEmpty', + desc: 'The error message when user connect with an empty server url', + ); String get bearerToken => Intl.message( - 'Bearer Token', - name: 'bearerToken', - desc: 'The label of the bearer token form text field', - ); + 'Bearer Token', + name: 'bearerToken', + desc: 'The label of the bearer token form text field', + ); String get introduceBearerToken => Intl.message( - 'Enter Bearer Token', - name: 'introduceBearerToken', - desc: 'The hint text of the bearer token form text field', - ); + 'Enter Bearer Token', + name: 'introduceBearerToken', + desc: 'The hint text of the bearer token form text field', + ); String get bearerTokenFormEmpty => Intl.message( - 'Enter a Bearer token to connect', - name: 'bearerTokenFormEmpty', - desc: 'The error message when user connect with an empty bearer token', - ); + 'Enter a Bearer token to connect', + name: 'bearerTokenFormEmpty', + desc: 'The error message when user connect with an empty bearer token', + ); String get loginWithAzureIdentity => Intl.message( - 'Login with Azure Identity', - name: 'loginWithAzureIdentity', - desc: 'The title of the button to change to azure identity login form', - ); + 'Login with Azure Identity', + name: 'loginWithAzureIdentity', + desc: 'The title of the button to change to azure identity login form', + ); String get loginWithBearerToken => Intl.message( - 'Login with Bearer token', - name: 'loginWithBearerToken', - desc: 'The title of the button to change to bearer token login form', - ); + 'Login with Bearer token', + name: 'loginWithBearerToken', + desc: 'The title of the button to change to bearer token login form', + ); String get serverConnectedWarning => Intl.message( - 'Connected to server', - name: 'serverConnectedWarning', - desc: 'Message for showing in a snackbar after a successful connection', - ); + 'Connected to server', + name: 'serverConnectedWarning', + desc: 'Message for showing in a snackbar after a successful connection', + ); String get serverNotConnectedWarning => Intl.message( - 'Cannot connect to specified URL', - name: 'serverNotConnectedWarning', - desc: - 'Message for showing in a snackbar after an unsuccessful connection', - ); + 'Cannot connect to specified URL', + name: 'serverNotConnectedWarning', + desc: 'Message for showing in a snackbar after an unsuccessful connection', + ); String get connectToServerHelpText => Intl.message( - '''Connect to the server to view all resources. + '''Connect to the server to view all resources. Copy and paste the link to your server's URL above.''', - name: 'connectToServerHelpText', - desc: - 'Message showed when the user does not have a valid connection to a server', - ); + name: 'connectToServerHelpText', + desc: + 'Message showed when the user does not have a valid connection to a server', + ); String get noDataAssociatedToAnEntity => Intl.message( - '''There's no data associated + '''There's no data associated with this entity''', - name: 'noDataAssociatedToAnEntity', - desc: - 'Message showed when no data retrieved from backend for a certcain entity', - ); + name: 'noDataAssociatedToAnEntity', + desc: + 'Message showed when no data retrieved from backend for a certcain entity', + ); String entityTypeIdHeader(final String entityType) => Intl.message( - '$entityType ID', - name: 'entityTypeIdHeader', - args: [entityType], - desc: 'ID column for the data viewer', - ); + '$entityType ID', + name: 'entityTypeIdHeader', + args: [entityType], + desc: 'ID column for the data viewer', + ); String get lastUpdate => Intl.message( - 'Last update', - name: 'lastUpdate', - desc: 'Last update column for the data viewer', - ); + 'Last update', + name: 'lastUpdate', + desc: 'Last update column for the data viewer', + ); String get selectAnEntityFromList => Intl.message( - 'Select an entity to see entries of that type', - name: 'selectAnEntityFromList', - desc: - 'Message showed when user is connected but not selected any entity to display data yet', - ); - - String get pageComingSoon => Intl.message('Page coming soon', - name: 'pageComingSoon', - desc: 'Message to show for pages that are not integrated yet'); - - String get server => Intl.message( - 'Server', - name: 'server', - desc: 'Server menu item label', - ); + 'Select an entity to see entries of that type', + name: 'selectAnEntityFromList', + desc: + 'Message showed when user is connected but not selected any entity to display data yet', + ); + + String get pageComingSoon => Intl.message( + 'Page coming soon', + name: 'pageComingSoon', + desc: 'Message to show for pages that are not integrated yet', + ); + + String get server => + Intl.message('Server', name: 'server', desc: 'Server menu item label'); String get license => Intl.message( - '''MIT License + '''MIT License Copyright (c) 2024 Evoleen Technology GmbH Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: @@ -219,101 +220,113 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ''', - name: 'license', - desc: 'License menu item label', - ); + name: 'license', + desc: 'License menu item label', + ); String get thirdPartyLicenses => Intl.message( - 'Third-Party Licenses', - name: 'thirdPartyLicenses', - desc: 'third party licenses menu item label', - ); + 'Third-Party Licenses', + name: 'thirdPartyLicenses', + desc: 'third party licenses menu item label', + ); String get saveChanges => Intl.message( - 'Save changes', - name: 'saveChanges', - desc: 'Save changes button title in json code editor', - ); + 'Save changes', + name: 'saveChanges', + desc: 'Save changes button title in json code editor', + ); String get invalidFhirJsonFormat => Intl.message( - 'Invalid FHIR JSON Format', - name: 'invalidFhirJsonFormat', - desc: - 'Error message when json code editor does not fhir FHIR structure', - ); + 'Invalid FHIR JSON Format', + name: 'invalidFhirJsonFormat', + desc: 'Error message when json code editor does not fhir FHIR structure', + ); String invalidJsonFormat(final String error) => Intl.message( - 'Invalid JSON Format: $error', - name: 'invalidJsonFormat', - desc: 'Error message when json code editor does not JSON structure', - args: [error], - ); + 'Invalid JSON Format: $error', + name: 'invalidJsonFormat', + desc: 'Error message when json code editor does not JSON structure', + args: [error], + ); String get errorWhenUpdatingResource => Intl.message( - 'Error when updating resource, please try again', - name: 'errorWhenUpdatingResource', - desc: - 'Error message when json is syntax correct but cannot be updated on backend', - ); + 'Error when updating resource, please try again', + name: 'errorWhenUpdatingResource', + desc: + 'Error message when json is syntax correct but cannot be updated on backend', + ); String get errorWhenDeletingResource => Intl.message( - 'Error when deleting resource, please try again', - name: 'errorWhenDeletingResource', - desc: 'Error message when a deletion request is rejected by the server', - ); + 'Error when deleting resource, please try again', + name: 'errorWhenDeletingResource', + desc: 'Error message when a deletion request is rejected by the server', + ); String get warningResourceIdChanged => Intl.message( - 'The resource\'s id was modified, the update will be saved as new resource or be rejected by the server.\nDo you want to continue?', - name: 'warningResourceIdChanged', - desc: - 'Warning message to show when user try to publish an entity with a different ID than original', - ); + 'The resource\'s id was modified, the update will be saved as new resource or be rejected by the server.\nDo you want to continue?', + name: 'warningResourceIdChanged', + desc: + 'Warning message to show when user try to publish an entity with a different ID than original', + ); String get warningResourceTypeChanged => Intl.message( - 'The resource\'s type was modified, the update will be saved as new resource or be rejected by the server.\nDo you want to continue?', - name: 'warningResourceTypeChanged', - desc: - 'Warning message to show when user try to publish an entity with a different resource type than original', - ); + 'The resource\'s type was modified, the update will be saved as new resource or be rejected by the server.\nDo you want to continue?', + name: 'warningResourceTypeChanged', + desc: + 'Warning message to show when user try to publish an entity with a different resource type than original', + ); String get noData => Intl.message( - '--', - name: 'noData', - desc: - 'Text to display in entity list view when no data available for any of its columns', - ); + '--', + name: 'noData', + desc: + 'Text to display in entity list view when no data available for any of its columns', + ); String get warning => Intl.message( - 'Warning', - name: 'warning', - desc: 'Title of the warning dialog', - ); + 'Warning', + name: 'warning', + desc: 'Title of the warning dialog', + ); String get cancel => Intl.message( - 'Cancel', - name: 'cancel', - desc: 'Title of the cancel button in the warning dialog', - ); + 'Cancel', + name: 'cancel', + desc: 'Title of the cancel button in the warning dialog', + ); String get proceed => Intl.message( - 'Continue', - name: 'proceed', - desc: 'Title of the continue button in the warning dialog', - ); + 'Continue', + name: 'proceed', + desc: 'Title of the continue button in the warning dialog', + ); String createNewResource(final String resourceType) => Intl.message( - 'Create new $resourceType', - name: 'createNewResource', - desc: 'Title of the create resource button', - args: [resourceType], - ); + 'Create new $resourceType', + name: 'createNewResource', + desc: 'Title of the create resource button', + args: [resourceType], + ); String resourceCreationSuccessful( - final String resourceType, final String resourceId) => - Intl.message( - '$resourceType $resourceId created succesfuly', - name: 'resourceCreationSuccessful', - desc: 'Success message to show in after create a new resource', - args: [resourceType, resourceId], - ); + final String resourceType, + final String resourceId, + ) => Intl.message( + '$resourceType $resourceId created succesfuly', + name: 'resourceCreationSuccessful', + desc: 'Success message to show in after create a new resource', + args: [resourceType, resourceId], + ); + + String get importExport => Intl.message( + 'Import / Export', + name: 'importExport', + desc: 'Import/Export menu item label', + ); + + String get importExportDescription => Intl.message( + 'Import and export FHIR resources to and from the server', + name: 'importExportDescription', + desc: 'Description text for the Import/Export page', + ); } diff --git a/lib/l10n/l10n/intl_messages.arb b/lib/l10n/l10n/intl_messages.arb index d0651db..99417c8 100644 --- a/lib/l10n/l10n/intl_messages.arb +++ b/lib/l10n/l10n/intl_messages.arb @@ -218,5 +218,17 @@ "resourceType": {}, "resourceId": {} } + }, + "importExport": "Import / Export", + "@importExport": { + "description": "Import/Export menu item label", + "type": "text", + "placeholders": {} + }, + "importExportDescription": "Import and export FHIR resources to and from various formats", + "@importExportDescription": { + "description": "Description text for the Import/Export page", + "type": "text", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/routes.dart b/lib/routes.dart index a8c0f4e..3ff3bab 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -1,5 +1,6 @@ import 'package:fire_scribe/app_scaffold.dart'; import 'package:fire_scribe/dashboard/dashboard_page.dart'; +import 'package:fire_scribe/import_export/import_export_page.dart'; import 'package:fire_scribe/license/pages/mic_oss_license_single_page.dart'; import 'package:fire_scribe/license/pages/third_party_licenses_page.dart'; import 'package:flutter/material.dart'; @@ -10,6 +11,7 @@ part 'routes.g.dart'; @TypedShellRoute( routes: [ TypedGoRoute(path: '/dashboard'), + TypedGoRoute(path: '/import-export'), TypedGoRoute( path: '/third-party-licenses', routes: [ @@ -36,6 +38,14 @@ class DashboardRoute extends GoRouteData with _$DashboardRoute { } } +@immutable +class ImportExportRoute extends GoRouteData with _$ImportExportRoute { + @override + Page buildPage(BuildContext context, GoRouterState state) { + return NoTransitionPage(child: AppScaffold(child: ImportExportPage())); + } +} + @immutable class ThirdPartyLicensesRoute extends GoRouteData with _$ThirdPartyLicensesRoute { diff --git a/lib/routes.g.dart b/lib/routes.g.dart index 1fe85d4..898e9dc 100644 --- a/lib/routes.g.dart +++ b/lib/routes.g.dart @@ -17,6 +17,10 @@ RouteBase get $homeRoute => ShellRouteData.$route( path: '/dashboard', factory: _$DashboardRoute._fromState, ), + GoRouteData.$route( + path: '/import-export', + factory: _$ImportExportRoute._fromState, + ), GoRouteData.$route( path: '/third-party-licenses', factory: _$ThirdPartyLicensesRoute._fromState, @@ -56,6 +60,29 @@ mixin _$DashboardRoute on GoRouteData { void replace(BuildContext context) => context.replace(location); } +mixin _$ImportExportRoute on GoRouteData { + static ImportExportRoute _fromState(GoRouterState state) => + ImportExportRoute(); + + @override + String get location => GoRouteData.$location( + '/import-export', + ); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + mixin _$ThirdPartyLicensesRoute on GoRouteData { static ThirdPartyLicensesRoute _fromState(GoRouterState state) => ThirdPartyLicensesRoute(); From 3c3a1c4b3f69fe037289b2a3933af5379a640a1c Mon Sep 17 00:00:00 2001 From: Till Gerken Date: Wed, 25 Jun 2025 17:57:58 +0200 Subject: [PATCH 6/9] Add file picker to import page --- lib/import_export/import_export_page.dart | 116 ++++++++++++++++-- lib/l10n/app_localizations.dart | 24 ++++ macos/Flutter/GeneratedPluginRegistrant.swift | 2 + macos/Podfile.lock | 6 + pubspec.lock | 16 +++ pubspec.yaml | 1 + 6 files changed, 152 insertions(+), 13 deletions(-) diff --git a/lib/import_export/import_export_page.dart b/lib/import_export/import_export_page.dart index a31dd62..5832257 100644 --- a/lib/import_export/import_export_page.dart +++ b/lib/import_export/import_export_page.dart @@ -1,3 +1,4 @@ +import 'package:file_picker/file_picker.dart'; import 'package:fire_scribe/l10n/app_localizations.dart'; import 'package:flutter/material.dart'; @@ -6,31 +7,120 @@ import 'package:flutter/material.dart'; /// This page provides a user interface for importing FHIR resources from external /// sources and exporting resources to various formats. The content will be /// implemented in subsequent steps. -class ImportExportPage extends StatelessWidget { +class ImportExportPage extends StatefulWidget { const ImportExportPage({super.key}); + @override + State createState() => _ImportExportPageState(); +} + +class _ImportExportPageState extends State { + PlatformFile? selectedFile; + + /// Opens a file picker to select an NDJSON file + Future selectFile() async { + try { + final result = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['ndjson', 'json'], + allowMultiple: false, + ); + + if (result != null && result.files.isNotEmpty) { + setState(() { + selectedFile = result.files.first; + }); + } + } catch (e) { + // Handle file picker errors silently for now + // In a production app, you might want to show a snackbar or dialog + } + } + + /// Handles the import button press + void handleImport() { + // TODO: Implement import functionality + // This will be implemented in subsequent steps + } + @override Widget build(BuildContext context) { return Scaffold( - body: Center( + body: Padding( + padding: const EdgeInsets.all(24.0), child: Column( - mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon( - Icons.import_export, - size: 64, - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(height: 16), - Text( - S.of(context).importExport, - style: Theme.of(context).textTheme.headlineMedium, + // Header section + Row( + children: [ + Icon( + Icons.import_export, + size: 32, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(width: 12), + Text( + S.of(context).importExport, + style: Theme.of(context).textTheme.headlineMedium, + ), + ], ), const SizedBox(height: 8), Text( S.of(context).importExportDescription, style: Theme.of(context).textTheme.bodyLarge, - textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + + // File picker section + Text( + S.of(context).ndjsonFileToImport, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.outline, + ), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + selectedFile?.name ?? S.of(context).noFileSelected, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: + selectedFile != null + ? Theme.of(context).colorScheme.onSurface + : Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ), + ), + ), + const SizedBox(width: 12), + OutlinedButton.icon( + onPressed: selectFile, + icon: const Icon(Icons.folder_open), + label: Text(S.of(context).selectFile), + ), + ], + ), + const SizedBox(height: 24), + + // Import button + FilledButton.icon( + onPressed: selectedFile != null ? handleImport : null, + icon: const Icon(Icons.upload), + label: Text(S.of(context).import), ), ], ), diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 1f63332..1ee840e 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -329,4 +329,28 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI name: 'importExportDescription', desc: 'Description text for the Import/Export page', ); + + String get ndjsonFileToImport => Intl.message( + 'NDJSON file to import', + name: 'ndjsonFileToImport', + desc: 'Label for the file picker input field for NDJSON import', + ); + + String get selectFile => Intl.message( + 'Select File', + name: 'selectFile', + desc: 'Button text for opening the file picker', + ); + + String get import => Intl.message( + 'Import', + name: 'import', + desc: 'Button text for importing the selected file', + ); + + String get noFileSelected => Intl.message( + 'No file selected', + name: 'noFileSelected', + desc: 'Placeholder text when no file is selected', + ); } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index c61cb1a..a35ca25 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,7 @@ import FlutterMacOS import Foundation +import file_picker import package_info_plus import path_provider_foundation import share_plus @@ -12,6 +13,7 @@ import shared_preferences_foundation import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 1574ec7..9c4e167 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,4 +1,6 @@ PODS: + - file_picker (0.0.1): + - FlutterMacOS - FlutterMacOS (1.0.0) - package_info_plus (0.0.1): - FlutterMacOS @@ -14,6 +16,7 @@ PODS: - FlutterMacOS DEPENDENCIES: + - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) @@ -22,6 +25,8 @@ DEPENDENCIES: - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) EXTERNAL SOURCES: + file_picker: + :path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos FlutterMacOS: :path: Flutter/ephemeral package_info_plus: @@ -36,6 +41,7 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos SPEC CHECKSUMS: + file_picker: a041d1192928187f68dfeee136d593da11a480f2 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 package_info_plus: f0052d280d17aa382b932f399edf32507174e870 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 diff --git a/pubspec.lock b/pubspec.lock index d076a40..37759a7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -367,6 +367,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: "3ddd0838d4fd028e23771a9e5f83f4ec6b3d73919cd1d72ae02709a47c1fcf7d" + url: "https://pub.dev" + source: hosted + version: "8.2.0" fixnum: dependency: transitive description: @@ -433,6 +441,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.4" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e + url: "https://pub.dev" + source: hosted + version: "2.0.28" flutter_staggered_grid_view: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f9a0738..046dbfc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,6 +22,7 @@ dependencies: url: https://github.com/evoleen/fhir_rest_client.git ref: add_docs fhir: 0.12.0 + file_picker: 8.2.0 flutter: sdk: flutter flutter_bloc: 9.1.1 From ded3df46a545de03fd98bf905e21a629bb3cc76d Mon Sep 17 00:00:00 2001 From: Till Gerken Date: Wed, 25 Jun 2025 18:53:27 +0200 Subject: [PATCH 7/9] Add proper user feedback --- lib/import_export/import_export_page.dart | 208 ++++++++++++++++------ lib/l10n/app_localizations.dart | 53 ++++++ 2 files changed, 206 insertions(+), 55 deletions(-) diff --git a/lib/import_export/import_export_page.dart b/lib/import_export/import_export_page.dart index 5832257..1857982 100644 --- a/lib/import_export/import_export_page.dart +++ b/lib/import_export/import_export_page.dart @@ -1,6 +1,8 @@ -import 'package:file_picker/file_picker.dart'; +import 'package:dio/dio.dart'; +import 'package:fire_scribe/auth/cubit/fhir_server_connection_cubit.dart'; import 'package:fire_scribe/l10n/app_localizations.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; /// A page for handling import and export functionality of FHIR resources. /// @@ -15,34 +17,150 @@ class ImportExportPage extends StatefulWidget { } class _ImportExportPageState extends State { - PlatformFile? selectedFile; + final TextEditingController _urlController = TextEditingController(); + final FocusNode _urlFocusNode = FocusNode(); + + @override + void dispose() { + _urlController.dispose(); + _urlFocusNode.dispose(); + super.dispose(); + } + + /// Handles the import button press + Future handleImport() async { + final url = _urlController.text.trim(); + if (url.isEmpty) { + return; + } - /// Opens a file picker to select an NDJSON file - Future selectFile() async { try { - final result = await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: ['ndjson', 'json'], - allowMultiple: false, + final connectionCubit = BlocProvider.of( + context, + ); + + // Check if we're authenticated + final isAuthenticated = connectionCubit.state.maybeWhen( + authenticated: (_, __) => true, + orElse: () => false, + ); + + if (!isAuthenticated) { + // Show error message that user needs to be connected to a server + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text(S.of(context).connectionRequired), + content: Text(S.of(context).connectionRequiredMessage), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(S.of(context).ok), + ), + ], + ); + }, + ); + return; + } + + // Get the FHIR client from the authenticated state + final fhirClient = connectionCubit.state.maybeWhen( + authenticated: (_, client) => client, + orElse: () => null, + ); + + if (fhirClient == null) { + // TODO: Show error message + return; + } + + // Make the import request using the underlying Dio client + final response = await fhirClient.dio.post( + '/\$import', + data: { + 'resourceType': 'Parameters', + 'parameter': [ + {'name': 'inputFormat', 'valueString': 'application/fhir+ndjson'}, + { + 'name': 'input', + 'part': [ + {'name': 'url', 'valueUri': url}, + ], + }, + ], + }, + options: Options( + headers: { + 'Prefer': 'respond-async', + 'Content-Type': 'application/fhir+json', + }, + ), ); - if (result != null && result.files.isNotEmpty) { - setState(() { - selectedFile = result.files.first; - }); + // Handle the response based on status code + if (response.statusCode.toString().startsWith("2")) { + // Success - import started + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text(S.of(context).importStarted), + content: Text(S.of(context).importStartedMessage), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(S.of(context).ok), + ), + ], + ); + }, + ); + } else { + // Unexpected response - show failure + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text(S.of(context).importFailed), + content: Text( + S + .of(context) + .importFailedMessage( + 'Status: ${response.statusCode}\nResponse: ${response.data}', + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(S.of(context).ok), + ), + ], + ); + }, + ); } } catch (e) { - // Handle file picker errors silently for now - // In a production app, you might want to show a snackbar or dialog + // Show error dialog with the exception details + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text(S.of(context).importError), + content: Text(S.of(context).importErrorMessage(e.toString())), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(S.of(context).ok), + ), + ], + ); + }, + ); } } - /// Handles the import button press - void handleImport() { - // TODO: Implement import functionality - // This will be implemented in subsequent steps - } - @override Widget build(BuildContext context) { return Scaffold( @@ -73,52 +191,32 @@ class _ImportExportPageState extends State { ), const SizedBox(height: 32), - // File picker section + // URL input section Text( S.of(context).ndjsonFileToImport, style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 12), - Row( - children: [ - Expanded( - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).colorScheme.outline, - ), - borderRadius: BorderRadius.circular(8), - ), - child: Text( - selectedFile?.name ?? S.of(context).noFileSelected, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: - selectedFile != null - ? Theme.of(context).colorScheme.onSurface - : Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), - ), - ), - ), - const SizedBox(width: 12), - OutlinedButton.icon( - onPressed: selectFile, - icon: const Icon(Icons.folder_open), - label: Text(S.of(context).selectFile), + TextField( + controller: _urlController, + focusNode: _urlFocusNode, + decoration: InputDecoration( + hintText: 'https://example.com/fhir/resources.ndjson', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), ), - ], + prefixIcon: const Icon(Icons.link), + ), + keyboardType: TextInputType.url, + textInputAction: TextInputAction.done, + onSubmitted: (_) => handleImport(), ), const SizedBox(height: 24), // Import button FilledButton.icon( - onPressed: selectedFile != null ? handleImport : null, + onPressed: + _urlController.text.trim().isNotEmpty ? handleImport : null, icon: const Icon(Icons.upload), label: Text(S.of(context).import), ), diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 1ee840e..029987b 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -353,4 +353,57 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI name: 'noFileSelected', desc: 'Placeholder text when no file is selected', ); + + String get connectionRequired => Intl.message( + 'Connection Required', + name: 'connectionRequired', + desc: 'Title for the dialog shown when user needs to connect to a server', + ); + + String get connectionRequiredMessage => Intl.message( + 'You need to be connected to a FHIR server before being able to import or export data.', + name: 'connectionRequiredMessage', + desc: 'Message shown in the dialog when user needs to connect to a server', + ); + + String get ok => + Intl.message('OK', name: 'ok', desc: 'OK button text for dialogs'); + + String get importError => Intl.message( + 'Import Error', + name: 'importError', + desc: 'Title for the dialog shown when import fails', + ); + + String importErrorMessage(final String error) => Intl.message( + 'An error occurred while importing the data:\n\n$error', + name: 'importErrorMessage', + desc: 'Message shown in the dialog when import fails', + args: [error], + ); + + String get importStarted => Intl.message( + 'Import Started', + name: 'importStarted', + desc: 'Title for the dialog shown when import is successfully initiated', + ); + + String get importStartedMessage => Intl.message( + 'The import process has been started successfully. You can monitor the progress in your FHIR server.', + name: 'importStartedMessage', + desc: 'Message shown in the dialog when import is successfully initiated', + ); + + String get importFailed => Intl.message( + 'Import Failed', + name: 'importFailed', + desc: 'Title for the dialog shown when import request fails', + ); + + String importFailedMessage(final String details) => Intl.message( + 'The import request failed:\n\n$details', + name: 'importFailedMessage', + desc: 'Message shown in the dialog when import request fails', + args: [details], + ); } From 9075b694240bebee3a1bdb9bcc3295a53560f5ee Mon Sep 17 00:00:00 2001 From: Till Gerken Date: Fri, 27 Jun 2025 14:13:06 +0200 Subject: [PATCH 8/9] Fix strings, update localization --- lib/l10n/app_localizations.dart | 4 +- lib/l10n/l10n/intl_messages.arb | 94 +++++++++++++++++++++++++++++++-- 2 files changed, 93 insertions(+), 5 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 029987b..df5fa76 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -319,13 +319,13 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ); String get importExport => Intl.message( - 'Import / Export', + 'Bulk Import', name: 'importExport', desc: 'Import/Export menu item label', ); String get importExportDescription => Intl.message( - 'Import and export FHIR resources to and from the server', + 'Bulk import FHIR resources to the server', name: 'importExportDescription', desc: 'Description text for the Import/Export page', ); diff --git a/lib/l10n/l10n/intl_messages.arb b/lib/l10n/l10n/intl_messages.arb index 99417c8..6ee69f9 100644 --- a/lib/l10n/l10n/intl_messages.arb +++ b/lib/l10n/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2024-11-29T13:30:33.994568", + "@@last_modified": "2025-06-27T14:12:47.731158", "appTitle": "Fire Scribe", "@appTitle": { "description": "The title of the app", @@ -166,6 +166,12 @@ "type": "text", "placeholders": {} }, + "errorWhenDeletingResource": "Error when deleting resource, please try again", + "@errorWhenDeletingResource": { + "description": "Error message when a deletion request is rejected by the server", + "type": "text", + "placeholders": {} + }, "warningResourceIdChanged": "The resource's id was modified, the update will be saved as new resource or be rejected by the server.\nDo you want to continue?", "@warningResourceIdChanged": { "description": "Warning message to show when user try to publish an entity with a different ID than original", @@ -219,16 +225,98 @@ "resourceId": {} } }, - "importExport": "Import / Export", + "importExport": "Bulk Import", "@importExport": { "description": "Import/Export menu item label", "type": "text", "placeholders": {} }, - "importExportDescription": "Import and export FHIR resources to and from various formats", + "importExportDescription": "Bulk import FHIR resources to the server", "@importExportDescription": { "description": "Description text for the Import/Export page", "type": "text", "placeholders": {} + }, + "ndjsonFileToImport": "NDJSON file to import", + "@ndjsonFileToImport": { + "description": "Label for the file picker input field for NDJSON import", + "type": "text", + "placeholders": {} + }, + "selectFile": "Select File", + "@selectFile": { + "description": "Button text for opening the file picker", + "type": "text", + "placeholders": {} + }, + "import": "Import", + "@import": { + "description": "Button text for importing the selected file", + "type": "text", + "placeholders": {} + }, + "noFileSelected": "No file selected", + "@noFileSelected": { + "description": "Placeholder text when no file is selected", + "type": "text", + "placeholders": {} + }, + "connectionRequired": "Connection Required", + "@connectionRequired": { + "description": "Title for the dialog shown when user needs to connect to a server", + "type": "text", + "placeholders": {} + }, + "connectionRequiredMessage": "You need to be connected to a FHIR server before being able to import or export data.", + "@connectionRequiredMessage": { + "description": "Message shown in the dialog when user needs to connect to a server", + "type": "text", + "placeholders": {} + }, + "ok": "OK", + "@ok": { + "description": "OK button text for dialogs", + "type": "text", + "placeholders": {} + }, + "importError": "Import Error", + "@importError": { + "description": "Title for the dialog shown when import fails", + "type": "text", + "placeholders": {} + }, + "importErrorMessage": "An error occurred while importing the data:\n\n{error}", + "@importErrorMessage": { + "description": "Message shown in the dialog when import fails", + "type": "text", + "placeholders": { + "error": {} + } + }, + "importStarted": "Import Started", + "@importStarted": { + "description": "Title for the dialog shown when import is successfully initiated", + "type": "text", + "placeholders": {} + }, + "importStartedMessage": "The import process has been started successfully. You can monitor the progress in your FHIR server.", + "@importStartedMessage": { + "description": "Message shown in the dialog when import is successfully initiated", + "type": "text", + "placeholders": {} + }, + "importFailed": "Import Failed", + "@importFailed": { + "description": "Title for the dialog shown when import request fails", + "type": "text", + "placeholders": {} + }, + "importFailedMessage": "The import request failed:\n\n{details}", + "@importFailedMessage": { + "description": "Message shown in the dialog when import request fails", + "type": "text", + "placeholders": { + "details": {} + } } } \ No newline at end of file From 9690f5ba65ff0487d8b8805d2288c96697e48140 Mon Sep 17 00:00:00 2001 From: Till Gerken Date: Fri, 27 Jun 2025 15:09:19 +0200 Subject: [PATCH 9/9] Remove obsolete strings --- lib/l10n/app_localizations.dart | 12 ------------ lib/l10n/l10n/intl_messages.arb | 14 +------------- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index df5fa76..29faa2c 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -336,24 +336,12 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI desc: 'Label for the file picker input field for NDJSON import', ); - String get selectFile => Intl.message( - 'Select File', - name: 'selectFile', - desc: 'Button text for opening the file picker', - ); - String get import => Intl.message( 'Import', name: 'import', desc: 'Button text for importing the selected file', ); - String get noFileSelected => Intl.message( - 'No file selected', - name: 'noFileSelected', - desc: 'Placeholder text when no file is selected', - ); - String get connectionRequired => Intl.message( 'Connection Required', name: 'connectionRequired', diff --git a/lib/l10n/l10n/intl_messages.arb b/lib/l10n/l10n/intl_messages.arb index 6ee69f9..64acb28 100644 --- a/lib/l10n/l10n/intl_messages.arb +++ b/lib/l10n/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2025-06-27T14:12:47.731158", + "@@last_modified": "2025-06-27T15:08:59.528239", "appTitle": "Fire Scribe", "@appTitle": { "description": "The title of the app", @@ -243,24 +243,12 @@ "type": "text", "placeholders": {} }, - "selectFile": "Select File", - "@selectFile": { - "description": "Button text for opening the file picker", - "type": "text", - "placeholders": {} - }, "import": "Import", "@import": { "description": "Button text for importing the selected file", "type": "text", "placeholders": {} }, - "noFileSelected": "No file selected", - "@noFileSelected": { - "description": "Placeholder text when no file is selected", - "type": "text", - "placeholders": {} - }, "connectionRequired": "Connection Required", "@connectionRequired": { "description": "Title for the dialog shown when user needs to connect to a server",