From 6b241604e8e3337d466644d3b0df607759cf6344 Mon Sep 17 00:00:00 2001 From: tamslo Date: Mon, 13 Jan 2025 21:24:33 +0100 Subject: [PATCH] feat(app): use expansion tile on gene report --- app/integration_test/report_test.dart | 2 +- app/lib/common/widgets/subheader_divider.dart | 8 +- app/lib/l10n/app_en.arb | 15 +- app/lib/report/pages/report.dart | 233 ++++++++++-------- 4 files changed, 149 insertions(+), 109 deletions(-) diff --git a/app/integration_test/report_test.dart b/app/integration_test/report_test.dart index 92d56bea..abcf6dc9 100644 --- a/app/integration_test/report_test.dart +++ b/app/integration_test/report_test.dart @@ -31,7 +31,7 @@ void main() { home: Scaffold( body: Builder( builder: (context) { - return ReportPage(onlyShowWholeReport: true); + return ReportPage(allGenesInitiallyExpanded: true); }, ), ), diff --git a/app/lib/common/widgets/subheader_divider.dart b/app/lib/common/widgets/subheader_divider.dart index c5de4e4c..85c11be3 100644 --- a/app/lib/common/widgets/subheader_divider.dart +++ b/app/lib/common/widgets/subheader_divider.dart @@ -1,5 +1,10 @@ import '../module.dart'; +TextStyle subheaderDividerStyle({Color? color}) => + PharMeTheme.textTheme.bodySmall!.copyWith( + color: color ?? PharMeTheme.subheaderColor, + ); + class SubheaderDivider extends StatelessWidget { const SubheaderDivider({ this.text = '', @@ -25,8 +30,7 @@ class SubheaderDivider extends StatelessWidget { if (useLine) Divider(color: widgetColor, thickness: 0.5), Text( text, - style: - PharMeTheme.textTheme.bodySmall!.copyWith(color: widgetColor), + style: subheaderDividerStyle(color: widgetColor), textAlign: TextAlign.start, ), ], diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 1c489856..f14ada71 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -293,7 +293,7 @@ "@report_current_medications": {}, "report_all_medications": "all known medication interactions", "@report_all_medications": {}, - "report_gene_number": "{geneNumber, plural, =1{ (1 gene)} other{ ({geneNumber} genes)}}", + "report_gene_number": "{geneNumber, plural, =1{1 gene} other{{geneNumber} genes}}", "@report_gene_number": { "placeholders": { "geneNumber": { @@ -302,6 +302,15 @@ } } }, + "report_medication_number": "{medicationNumber, plural, =1{1 medication} other{{medicationNumber} medications}}", + "@report_medication_number": { + "placeholders": { + "medicationNumber": { + "type": "int", + "example": "3" + } + } + }, "show_all_dropdown_text": "Having trouble finding a {item}? Use the dropdown button (▾) {position} to show all {items}.", "@show_all_dropdown_text": { "placeholders": { @@ -315,11 +324,11 @@ }, "items": { "type": "String", - "example": "genes with known medication interactions" + "example": "medications with clinically relevant gene interactions" } } }, - "report_dropdown_position": "above", + "report_dropdown_position": "below the current medication interaction report", "@report_dropdown_position": {}, "medications_dropdown_position": "below the current medications", "@medications_dropdown_position": {}, diff --git a/app/lib/report/pages/report.dart b/app/lib/report/pages/report.dart index e2549ccf..a8c23f74 100644 --- a/app/lib/report/pages/report.dart +++ b/app/lib/report/pages/report.dart @@ -4,27 +4,6 @@ import 'package:provider/provider.dart'; import '../../common/module.dart'; typedef WarningLevelCounts = Map; -class ListOption { - ListOption({required this.label, this.drugSubset}); - Widget getDescription(BuildContext context, int geneNumber) { - return Text.rich( - style: PharMeTheme.textTheme.labelLarge, - TextSpan( - children: [ - TextSpan(text: context.l10n.report_description_prefix), - TextSpan(text: ' '), - TextSpan(text: label, style: TextStyle(fontWeight: FontWeight.bold)), - TextSpan( - text: context.l10n.report_gene_number(geneNumber), - style: TextStyle(color: PharMeTheme.buttonColor), - ), - ], - ), - ); - } - final String label; - final List? drugSubset; -} enum SortOption { alphabetical, @@ -33,22 +12,21 @@ enum SortOption { @RoutePage() class ReportPage extends HookWidget { - const ReportPage({@visibleForTesting this.onlyShowWholeReport = false}); + const ReportPage({@visibleForTesting this.allGenesInitiallyExpanded = false}); - // Currently for testing but might use in the future - final bool onlyShowWholeReport; + final bool allGenesInitiallyExpanded; @override Widget build(BuildContext context) { - final currentListOption = useState(0); - // Not used yet, but could be adaptable in the future + final allGenesExpanded = useState(allGenesInitiallyExpanded); + // Not changeable yet in UI! final currentSortOption = useState(SortOption.alphabetical); return Consumer( builder: (context, activeDrugs, child) => _buildReportPage( context, activeDrugs, - currentListOption, + allGenesExpanded, currentSortOption, ) ); @@ -76,24 +54,28 @@ class ReportPage extends HookWidget { return allAffectedDrugs; } - Iterable _getRelevantGenotypes(List? drugSubset) { - return UserData.instance.genotypeResults == null - ? [] - : drugSubset != null - ? UserData.instance.genotypeResults!.values.filter((genotypeResult) => - _getAffectedDrugs( - genotypeResult.key.value, - drugSubset: drugSubset, - ).isNotEmpty - ) - : UserData.instance.genotypeResults!.values; + Iterable _getRelevantGenotypes( + List? drugSubset, + ) { + if (UserData.instance.genotypeResults == null) return []; + final allGenotypeResults = UserData.instance.genotypeResults!.values; + if (drugSubset == null) return allGenotypeResults; + return allGenotypeResults.filter( + (genotypeResult) => _getAffectedDrugs( + genotypeResult.key.value, + drugSubset: drugSubset, + ).isNotEmpty + ); } List _buildGeneCards({ required SortOption currentSortOption, List? drugsToFilterBy, + required String keyPostfix, }) { - final userGenotypes = _getRelevantGenotypes(drugsToFilterBy); + final userGenotypes = _getRelevantGenotypes( + drugsToFilterBy, + ); final warningLevelCounts = {}; for (final genotypeResult in userGenotypes) { warningLevelCounts[genotypeResult.key.value] = {}; @@ -130,7 +112,7 @@ class ReportPage extends HookWidget { GeneCard( genotypeResult, warningLevelCounts[genotypeResult.key.value]!, - key: Key('gene-card-${genotypeResult.key.value}'), + key: Key('gene-card-${genotypeResult.key.value}-$keyPostfix'), useColors: false, ) ).toList(); @@ -138,20 +120,21 @@ class ReportPage extends HookWidget { Widget _maybeBuildPageIndicators( BuildContext context, - Iterable relevantGenes, ActiveDrugs activeDrugs, - ListOption currentListOption, + { required bool allGenesVisible } ) { + final drugsToFilterBy = allGenesVisible ? null : activeDrugs.names; + final relevantGenes = _getRelevantGenotypes(drugsToFilterBy); final hasActiveInhibitors = relevantGenes.any( (genotypeResult) => activeDrugs.names.any( (drug) => isInhibited(genotypeResult, drug: drug) ) ); - if (!hasActiveInhibitors && currentListOption.drugSubset == null) { + if (!hasActiveInhibitors && drugsToFilterBy == null) { return SizedBox.shrink(); } var indicatorText = ''; - if (currentListOption.drugSubset != null) { + if (drugsToFilterBy != null) { final listHelperText = context.l10n.show_all_dropdown_text( context.l10n.report_show_all_dropdown_item, context.l10n.report_dropdown_position, @@ -176,24 +159,9 @@ class ReportPage extends HookWidget { Widget _buildReportPage( BuildContext context, ActiveDrugs activeDrugs, - ValueNotifier currentListOptionIndex, + ValueNotifier allGenesExpanded, ValueNotifier currentSortOption, ) { - final listOptions = onlyShowWholeReport - ? [ListOption(label: context.l10n.report_all_medications)] - : [ - ListOption( - label: context.l10n.report_current_medications, - drugSubset: activeDrugs.names, - ), - ListOption(label: context.l10n.report_all_medications), - ]; - final currentListOption = listOptions[currentListOptionIndex.value]; - final geneCards = _buildGeneCards( - currentSortOption: currentSortOption.value, - drugsToFilterBy: currentListOption.drugSubset, - ); - final relevantGenes = _getRelevantGenotypes(currentListOption.drugSubset); return PopScope( canPop: false, child: unscrollablePageScaffold( @@ -204,60 +172,119 @@ class ReportPage extends HookWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ PageDescription.fromText(context.l10n.report_content_explanation), - Padding( - key: Key('gene-report-selection'), - padding: EdgeInsets.only( - top: PharMeTheme.smallSpace, - left: PharMeTheme.smallSpace, - bottom: PharMeTheme.smallSpace, - right: PharMeTheme.mediumToLargeSpace, - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - isExpanded: true, - isDense: false, - value: currentListOptionIndex.value, - onChanged: (index) => currentListOptionIndex.value = - index ?? currentListOptionIndex.value, - icon: Padding( - padding: EdgeInsets.only(left: PharMeTheme.smallSpace), - child: ResizedIconButton( - size: PharMeTheme.largeSpace, - disabledBackgroundColor: PharMeTheme.buttonColor, - iconWidgetBuilder: (size) => Icon( - Icons.arrow_drop_down, - size: size, - color: PharMeTheme.surfaceColor, - ), - ), - ), - items: listOptions.mapIndexed( - (index, listOption) => DropdownMenuItem( - value: index, - child: listOption.getDescription( - context, - _buildGeneCards( - drugsToFilterBy: listOption.drugSubset, - currentSortOption: currentSortOption.value, - ).length - ), - ), - ).toList(), - ), + scrollList( + _buildReportLists( + context, + activeDrugs, + allGenesExpanded, + currentSortOption, ), ), - scrollList(geneCards), _maybeBuildPageIndicators( context, - relevantGenes, activeDrugs, - currentListOption, + allGenesVisible: allGenesExpanded.value, ), ] ) ), ); } + + Widget _listDescription( + BuildContext context, + String label, + { required List? drugsToFilterBy } + ) { + final genotypes = _getRelevantGenotypes( + drugsToFilterBy, + ); + final affectedDrugs = genotypes.flatMap( + (genotypeResult) => _getAffectedDrugs( + genotypeResult.key.value, + drugSubset: drugsToFilterBy, + ) + ).toSet(); + return Padding( + key: Key('list-description-$label'), + padding: EdgeInsets.symmetric( + vertical: PharMeTheme.mediumSpace, + horizontal: PharMeTheme.smallSpace, + ), + child: Text.rich( + style: subheaderDividerStyle(), + TextSpan( + children: [ + TextSpan(text: context.l10n.report_description_prefix), + TextSpan(text: ' '), + TextSpan(text: label, style: TextStyle(fontWeight: FontWeight.bold)), + TextSpan(text: ' '), + TextSpan( + text: '(${context.l10n.report_gene_number(genotypes.length)}, ' + '${context.l10n.report_medication_number(affectedDrugs.length)})', + style: TextStyle(color: PharMeTheme.buttonColor), + ), + ], + ), + ), + ); + } + + List _buildReportLists( + BuildContext context, + ActiveDrugs activeDrugs, + ValueNotifier allGenesExpanded, + ValueNotifier currentSortOption, + ) { + final currentMedicationGenes = _buildGeneCards( + currentSortOption: currentSortOption.value, + drugsToFilterBy: activeDrugs.names, + keyPostfix: 'current-medications', + ); + final allMedicationGenesHeader = _listDescription( + context, + context.l10n.report_all_medications, + drugsToFilterBy: null, + ); + final allMedicationGenes = _buildGeneCards( + currentSortOption: currentSortOption.value, + drugsToFilterBy: null, + keyPostfix: 'all-medications', + ); + if (currentMedicationGenes.isEmpty) { + return [ + allMedicationGenesHeader, + ...allMedicationGenes, + ]; + } + return [ + _listDescription( + context, + context.l10n.report_current_medications, + drugsToFilterBy: activeDrugs.names, + ), + ...currentMedicationGenes, + PrettyExpansionTile( + title: allMedicationGenesHeader, + initiallyExpanded: allGenesExpanded.value, + onExpansionChanged: (value) => allGenesExpanded.value = value, + titlePadding: EdgeInsets.zero, + childrenPadding: EdgeInsets.zero, + icon: ResizedIconButton( + size: PharMeTheme.largeSpace, + disabledBackgroundColor: PharMeTheme.buttonColor, + iconWidgetBuilder: (size) => Icon( + allGenesExpanded.value + ? Icons.arrow_drop_up + : Icons.arrow_drop_down, + size: size, + color: PharMeTheme.surfaceColor, + ), + ), + children: allMedicationGenes, + ), + ]; + } } bool _hasNoResult(GenotypeResult genotypeResult) =>