From 99b155484bd6b3adc5ead9a9dd6490b6f4a203dc Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Sat, 18 Feb 2023 14:32:54 +0100 Subject: [PATCH 01/35] change(#378): disable deleting bricks --- anni/src/components/bricks/BrickForm.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/anni/src/components/bricks/BrickForm.tsx b/anni/src/components/bricks/BrickForm.tsx index bd01b92ab..37b6096f9 100644 --- a/anni/src/components/bricks/BrickForm.tsx +++ b/anni/src/components/bricks/BrickForm.tsx @@ -66,6 +66,7 @@ const BrickForm = ({ usage, brick }: Props) => { ): void => { setTranslations((prev) => new Map(prev).set(language, text)); }; + // eslint-disable-next-line @typescript-eslint/no-unused-vars const deleteTranslation = (language: SupportedLanguage): void => { setTranslations((prev) => { const n = new Map(prev); @@ -110,13 +111,15 @@ const BrickForm = ({ usage, brick }: Props) => {

{language}

+ {/* + Disabled for #378 until proper handling of deletion deleteTranslation(language)} > Delete translation - + */}
Date: Sat, 18 Feb 2023 14:37:42 +0100 Subject: [PATCH 02/35] fix(#378): actually prevent deleting bricks --- anni/src/components/bricks/BrickForm.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/anni/src/components/bricks/BrickForm.tsx b/anni/src/components/bricks/BrickForm.tsx index 37b6096f9..e431293fc 100644 --- a/anni/src/components/bricks/BrickForm.tsx +++ b/anni/src/components/bricks/BrickForm.tsx @@ -66,7 +66,6 @@ const BrickForm = ({ usage, brick }: Props) => { ): void => { setTranslations((prev) => new Map(prev).set(language, text)); }; - // eslint-disable-next-line @typescript-eslint/no-unused-vars const deleteTranslation = (language: SupportedLanguage): void => { setTranslations((prev) => { const n = new Map(prev); @@ -86,6 +85,7 @@ const BrickForm = ({ usage, brick }: Props) => { setMessage(`Failed to ${id ? 'update' : 'add new'} Brick.`); } }; + // eslint-disable-next-line @typescript-eslint/no-unused-vars const deleteBrick = async () => { try { await axios.delete(`/api/bricks/${id}`); @@ -111,15 +111,13 @@ const BrickForm = ({ usage, brick }: Props) => {

{language}

- {/* - Disabled for #378 until proper handling of deletion deleteTranslation(language)} > Delete translation - */} +
{ Cancel
- {id && ( + {/* + // Disabled for #378 until proper handling of deletion + id && ( { > Delete Brick - )} + )*/} Date: Sat, 18 Feb 2023 16:00:46 +0100 Subject: [PATCH 03/35] refactor(#484): save diplotypes as map --- app/lib/common/models/userdata/userdata.dart | 2 +- app/lib/common/utilities/genome_data.dart | 17 ++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/app/lib/common/models/userdata/userdata.dart b/app/lib/common/models/userdata/userdata.dart index ebdfed26d..527bc9e84 100644 --- a/app/lib/common/models/userdata/userdata.dart +++ b/app/lib/common/models/userdata/userdata.dart @@ -30,7 +30,7 @@ class UserData { } @HiveField(0) - List? diplotypes; + Map? diplotypes; @HiveField(1) Map? lookups; diff --git a/app/lib/common/utilities/genome_data.dart b/app/lib/common/utilities/genome_data.dart index bd79a56e1..78b0293d9 100644 --- a/app/lib/common/utilities/genome_data.dart +++ b/app/lib/common/utilities/genome_data.dart @@ -26,7 +26,7 @@ Future _saveDiplotypeResponse(Response response) async { final diplotypes = diplotypesFromHTTPResponse(response).filterValidDiplotypes(); - UserData.instance.diplotypes = diplotypes; + UserData.instance.diplotypes = {for (var d in diplotypes) d.gene: d}; await UserData.save(); // invalidate cached drugs because lookups may have changed and we need to // refilter the matching guidelines @@ -55,14 +55,17 @@ Future fetchAndSaveLookups() async { // ignore: omit_local_variable_types final Map matchingLookups = {}; // extract the matching lookups - for (final diplotype in usersDiplotypes) { - if (diplotype.genotype.contains('-')) { - // TODO(kolioOtSofia): what to do with genotypes that contain - - } + for (final diplotype in usersDiplotypes.values) { // the gene and the genotype build the key for the hashmap final key = '${diplotype.gene}__${diplotype.genotype}'; - final temp = lookupsHashMap[key]; - if (temp != null) matchingLookups[diplotype.gene] = temp; + final lookup = lookupsHashMap[key]; + if (lookup == null) continue; + // uncomment to print literal mismatches between lab/CPIC phenotypes + // if (diplotype.phenotype != lookup.phenotype) { + // print( + // 'Lab phenotype ${diplotype.phenotype} for ${diplotype.gene} (${diplotype.genotype}) is "${lookup.phenotype}" for CPIC'); + // } + matchingLookups[diplotype.gene] = lookup; } UserData.instance.lookups = matchingLookups; From dcefa68fb9e59a1a2c7c861e6b9c38c29217322c Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Sat, 18 Feb 2023 16:05:17 +0100 Subject: [PATCH 04/35] feat(#484): show phenotype from lab --- .../common/pages/drug/widgets/annotation_cards/guideline.dart | 2 +- app/lib/report/pages/report.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/lib/common/pages/drug/widgets/annotation_cards/guideline.dart b/app/lib/common/pages/drug/widgets/annotation_cards/guideline.dart index 318121312..4401a16d9 100644 --- a/app/lib/common/pages/drug/widgets/annotation_cards/guideline.dart +++ b/app/lib/common/pages/drug/widgets/annotation_cards/guideline.dart @@ -56,7 +56,7 @@ class GuidelineAnnotationCard extends StatelessWidget { Widget _buildHeader(BuildContext context) { final geneDescriptions = guideline.lookupkey.keys.map((geneSymbol) => - '$geneSymbol (${UserData.instance.lookups![geneSymbol]!.phenotype})'); + '$geneSymbol (${UserData.instance.diplotypes![geneSymbol]!.phenotype})'); return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ SubHeader(context.l10n.drugs_page_your_genome), SizedBox(height: 12), diff --git a/app/lib/report/pages/report.dart b/app/lib/report/pages/report.dart index fa18251d9..6fce0660e 100644 --- a/app/lib/report/pages/report.dart +++ b/app/lib/report/pages/report.dart @@ -35,7 +35,7 @@ class GeneCard extends StatelessWidget { Text(phenotype.geneSymbol, style: PharMeTheme.textTheme.titleMedium), SizedBox(height: 8), - Text(phenotype.phenotype, + Text(UserData.instance.lookups![phenotype.geneSymbol]!.phenotype, style: PharMeTheme.textTheme.titleSmall), ], ), From 2cdeb7d9ae96e1a81ffdccae08d9d86ac9639e17 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Sat, 18 Feb 2023 19:10:04 +0100 Subject: [PATCH 05/35] test(#484): fix drug test --- app/integration_test/drugs_test.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/integration_test/drugs_test.dart b/app/integration_test/drugs_test.dart index e18c76c22..6e8199d7d 100644 --- a/app/integration_test/drugs_test.dart +++ b/app/integration_test/drugs_test.dart @@ -52,6 +52,14 @@ void main() { genotype: '*1/*1', lookupkey: 'Normal Metabolizer') }; + UserData.instance.diplotypes = { + 'CYP2C9': Diplotype( + gene: 'CYP2C9', + resultType: 'Diplotype', + phenotype: 'Normal Metabolizer', + genotype: '*1/*1', + allelesTested: '1') + }; final testDrugWithoutGuidelines = Drug( id: '2', version: 1, From 40496dbd575d1bc8cf32e7e5d34d55223e6b97bc Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Sat, 18 Feb 2023 19:00:11 +0100 Subject: [PATCH 06/35] feat(#443): dynamic guideline matching --- app/lib/common/models/drug/drug.dart | 50 ++++++------------------ app/lib/common/pages/drug/drug.dart | 5 ++- app/lib/common/utilities/drug_utils.dart | 4 +- app/lib/search/pages/search.dart | 2 +- 4 files changed, 16 insertions(+), 45 deletions(-) diff --git a/app/lib/common/models/drug/drug.dart b/app/lib/common/models/drug/drug.dart index 8350e05ab..81f12293e 100644 --- a/app/lib/common/models/drug/drug.dart +++ b/app/lib/common/models/drug/drug.dart @@ -80,52 +80,24 @@ extension DrugMatchesQuery on Drug { } } -/// Removes the guidelines that are not relevant to the user -extension DrugWithUserGuidelines on Drug { - Drug filterUserGuidelines() { - final matchingGuidelines = guidelines.where((guideline) { - // Guideline matches if all user has any of the gene results for all gene - // symbols - return guideline.lookupkey.all((geneSymbol, geneResults) => - (UserData.instance.lookups?.containsKey(geneSymbol) ?? false) && - geneResults - .contains(UserData.instance.lookups?[geneSymbol]?.lookupkey)); - }); - - return Drug( - id: id, - version: version, - name: name, - rxNorm: rxNorm, - annotations: annotations, - guidelines: matchingGuidelines.toList(), - ); - } -} - -/// Removes the guidelines that are not relevant to the user -extension DrugsWithUserGuidelines on List { - List filterUserGuidelines() { - return map((drug) => drug.filterUserGuidelines()).toList(); - } +/// Gets the User's matching guideline +extension DrugWithUserGuideline on Drug { + Guideline? userGuideline() => guidelines.firstOrNullWhere( + (guideline) => guideline.lookupkey.all( + (geneSymbol, geneResults) => + (UserData.instance.lookups?.containsKey(geneSymbol) ?? false) && + geneResults + .contains(UserData.instance.lookups?[geneSymbol]?.lookupkey), + ), + ); } /// Filters for drugs with non-OK warning level extension CriticalDrugs on List { List filterCritical() { return filter((drug) { - final warningLevel = drug.highestWarningLevel(); + final warningLevel = drug.userGuideline()?.annotations.warningLevel; return warningLevel != null && warningLevel != WarningLevel.green; }).toList(); } } - -/// Gets most severe warning level -extension DrugWarningLevel on Drug { - WarningLevel? highestWarningLevel() { - final filtered = filterUserGuidelines(); - return filtered.guidelines - .map((guideline) => guideline.annotations.warningLevel) - .maxBy((level) => level.severity); - } -} diff --git a/app/lib/common/pages/drug/drug.dart b/app/lib/common/pages/drug/drug.dart index 2da19ba3d..4fa0861b4 100644 --- a/app/lib/common/pages/drug/drug.dart +++ b/app/lib/common/pages/drug/drug.dart @@ -58,6 +58,7 @@ class DrugPage extends StatelessWidget { required bool isStarred, required BuildContext context, }) { + final userGuideline = drug.userGuideline(); return Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16), child: Column( @@ -72,11 +73,11 @@ class DrugPage extends StatelessWidget { tooltip: context.l10n.drugs_page_tooltip_guideline, ), SizedBox(height: 12), - ...(drug.guidelines.isNotEmpty) + ...(userGuideline != null) ? [ Disclaimer(), SizedBox(height: 12), - GuidelineAnnotationCard(drug.guidelines[0]) + GuidelineAnnotationCard(userGuideline) ] : [Text(context.l10n.drugs_page_no_guidelines_for_phenotype)] ], diff --git a/app/lib/common/utilities/drug_utils.dart b/app/lib/common/utilities/drug_utils.dart index 6c577b2ee..2b6d925e8 100644 --- a/app/lib/common/utilities/drug_utils.dart +++ b/app/lib/common/utilities/drug_utils.dart @@ -20,8 +20,6 @@ Future updateCachedDrugs() async { final dataResponse = await get(anniUrl('data')); if (dataResponse.statusCode != 200) throw Exception(); - final drugs = - AnniDataResponse.fromJson(jsonDecode(dataResponse.body)).data.drugs; - CachedDrugs.instance.drugs = drugs.filterUserGuidelines(); + CachedDrugs.instance.drugs = AnniDataResponse.fromJson(jsonDecode(dataResponse.body)).data.drugs; await CachedDrugs.save(); } diff --git a/app/lib/search/pages/search.dart b/app/lib/search/pages/search.dart index bb7a88a47..259aad0f7 100644 --- a/app/lib/search/pages/search.dart +++ b/app/lib/search/pages/search.dart @@ -76,7 +76,7 @@ class DrugCard extends StatelessWidget { @override Widget build(BuildContext context) { - final warningLevel = drug.highestWarningLevel(); + final warningLevel = drug.userGuideline()?.annotations.warningLevel; return RoundedCard( onTap: onTap, From fe0d054541e449fce164501c07bc27dbc4809f2f Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Sat, 18 Feb 2023 19:36:53 +0100 Subject: [PATCH 07/35] feat(#443): add drug inhibitor map --- .../common/models/drug/drug_inhibitors.dart | 21 +++++++++++++++++++ app/lib/common/models/module.dart | 1 + 2 files changed, 22 insertions(+) create mode 100644 app/lib/common/models/drug/drug_inhibitors.dart diff --git a/app/lib/common/models/drug/drug_inhibitors.dart b/app/lib/common/models/drug/drug_inhibitors.dart new file mode 100644 index 000000000..433814382 --- /dev/null +++ b/app/lib/common/models/drug/drug_inhibitors.dart @@ -0,0 +1,21 @@ +// Everything has to match literally. The final value is not a phenotype but +// the CPIC lookupkey value. +// structure: gene symbol -> drug name -> overwriting lookupkey + +const Map> drugInhibitors = { + 'CYP2D6': { + // 0.0 is a lookupkey for a type of poor metabolizer + 'bupropion': '0.0', + 'fluoxetine': '0.0', + 'paroxetine': '0.0', + 'quinidine': '0.0', + 'terbinafine': '0.0', + // 1.0 is a lookupkey for a type of poor (but less poor than 0.0) + // metabolizer + 'abiraterone': '1.0', + 'cinacalcet': '1.0', + 'duloxetine': '1.0', + 'lorcaserin': '1.0', + 'mirabegron': '1.0', + } +}; diff --git a/app/lib/common/models/module.dart b/app/lib/common/models/module.dart index 66ec1fd4c..30f7b4735 100644 --- a/app/lib/common/models/module.dart +++ b/app/lib/common/models/module.dart @@ -1,5 +1,6 @@ export 'anni_response.dart'; export 'drug/drug.dart'; +export 'drug/drug_inhibitors.dart'; export 'drug/guideline.dart'; export 'drug/warning_level.dart'; export 'metadata.dart'; From 22085dca5017c196c6f0bad50a078749411f34c6 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Sat, 18 Feb 2023 19:54:41 +0100 Subject: [PATCH 08/35] refactor(#443): get phenotype & lookupkey from userdata --- app/lib/common/models/drug/drug.dart | 8 ++------ app/lib/common/models/userdata/userdata.dart | 4 ++++ .../pages/drug/widgets/annotation_cards/guideline.dart | 2 +- app/lib/common/utilities/pdf_utils.dart | 2 +- app/lib/report/pages/gene.dart | 4 ++-- app/lib/report/pages/report.dart | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/lib/common/models/drug/drug.dart b/app/lib/common/models/drug/drug.dart index 81f12293e..2b22b669e 100644 --- a/app/lib/common/models/drug/drug.dart +++ b/app/lib/common/models/drug/drug.dart @@ -83,12 +83,8 @@ extension DrugMatchesQuery on Drug { /// Gets the User's matching guideline extension DrugWithUserGuideline on Drug { Guideline? userGuideline() => guidelines.firstOrNullWhere( - (guideline) => guideline.lookupkey.all( - (geneSymbol, geneResults) => - (UserData.instance.lookups?.containsKey(geneSymbol) ?? false) && - geneResults - .contains(UserData.instance.lookups?[geneSymbol]?.lookupkey), - ), + (guideline) => guideline.lookupkey.all((geneSymbol, geneResults) => + geneResults.contains(UserData.lookupFor(geneSymbol))), ); } diff --git a/app/lib/common/models/userdata/userdata.dart b/app/lib/common/models/userdata/userdata.dart index 527bc9e84..a00c70046 100644 --- a/app/lib/common/models/userdata/userdata.dart +++ b/app/lib/common/models/userdata/userdata.dart @@ -31,9 +31,13 @@ class UserData { @HiveField(0) Map? diplotypes; + static String? phenotypeFor(String gene) => + UserData.instance.diplotypes?[gene]?.phenotype; @HiveField(1) Map? lookups; + static String? lookupFor(String gene) => + UserData.instance.lookups?[gene]?.lookupkey; @HiveField(2) List? starredDrugIds; diff --git a/app/lib/common/pages/drug/widgets/annotation_cards/guideline.dart b/app/lib/common/pages/drug/widgets/annotation_cards/guideline.dart index 4401a16d9..2602cf18b 100644 --- a/app/lib/common/pages/drug/widgets/annotation_cards/guideline.dart +++ b/app/lib/common/pages/drug/widgets/annotation_cards/guideline.dart @@ -56,7 +56,7 @@ class GuidelineAnnotationCard extends StatelessWidget { Widget _buildHeader(BuildContext context) { final geneDescriptions = guideline.lookupkey.keys.map((geneSymbol) => - '$geneSymbol (${UserData.instance.diplotypes![geneSymbol]!.phenotype})'); + '$geneSymbol (${UserData.phenotypeFor('geneSymbol')!})'); return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ SubHeader(context.l10n.drugs_page_your_genome), SizedBox(height: 12), diff --git a/app/lib/common/utilities/pdf_utils.dart b/app/lib/common/utilities/pdf_utils.dart index b9671945a..e8733e62b 100644 --- a/app/lib/common/utilities/pdf_utils.dart +++ b/app/lib/common/utilities/pdf_utils.dart @@ -75,7 +75,7 @@ List _buildGuidelinePart(Guideline guideline) { title: 'Relevant gene phenotypes: ', text: guideline.lookupkey.keys .map((geneSymbol) => - '$geneSymbol: ${UserData.instance.lookups![geneSymbol]!}') + '$geneSymbol: ${UserData.phenotypeFor(geneSymbol)!}') .join(', ')), ), pw.SizedBox(height: 8, width: double.infinity), diff --git a/app/lib/report/pages/gene.dart b/app/lib/report/pages/gene.dart index 42258dacf..77c2a34df 100644 --- a/app/lib/report/pages/gene.dart +++ b/app/lib/report/pages/gene.dart @@ -31,8 +31,8 @@ class GenePage extends StatelessWidget { _buildRow( context.l10n.gene_page_genotype, phenotype.genotype, tooltip: context.l10n.gene_page_genotype_tooltip), - _buildRow( - context.l10n.gene_page_phenotype, phenotype.phenotype, + _buildRow(context.l10n.gene_page_phenotype, + UserData.phenotypeFor(phenotype.geneSymbol)!, tooltip: context.l10n.gene_page_phenotype_tooltip), ]), ), diff --git a/app/lib/report/pages/report.dart b/app/lib/report/pages/report.dart index 6fce0660e..41e1ee77b 100644 --- a/app/lib/report/pages/report.dart +++ b/app/lib/report/pages/report.dart @@ -35,7 +35,7 @@ class GeneCard extends StatelessWidget { Text(phenotype.geneSymbol, style: PharMeTheme.textTheme.titleMedium), SizedBox(height: 8), - Text(UserData.instance.lookups![phenotype.geneSymbol]!.phenotype, + Text(UserData.phenotypeFor(phenotype.geneSymbol)!, style: PharMeTheme.textTheme.titleSmall), ], ), From f4f8a02c2a7d8bd6472cdbe22d2d06fb287be0e9 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Sat, 18 Feb 2023 20:02:08 +0100 Subject: [PATCH 09/35] refactor(#443): starred drug ids -> names --- app/integration_test/drugs_test.dart | 2 +- app/lib/common/models/drug/drug.dart | 2 +- app/lib/common/models/userdata/userdata.dart | 2 +- app/lib/common/pages/drug/cubit.dart | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/integration_test/drugs_test.dart b/app/integration_test/drugs_test.dart index 6e8199d7d..d4b5fbbf1 100644 --- a/app/integration_test/drugs_test.dart +++ b/app/integration_test/drugs_test.dart @@ -71,7 +71,7 @@ void main() { brandNames: ['brand name', 'another brand name']), guidelines: [], ); - UserData.instance.starredDrugIds = ['1']; + UserData.instance.starredDrugNames = ['Ibuprofen']; group('integration test for the drugs page', () { testWidgets('test loading', (tester) async { diff --git a/app/lib/common/models/drug/drug.dart b/app/lib/common/models/drug/drug.dart index 2b22b669e..b69193d2d 100644 --- a/app/lib/common/models/drug/drug.dart +++ b/app/lib/common/models/drug/drug.dart @@ -68,7 +68,7 @@ class DrugAnnotations { extension DrugIsStarred on Drug { bool isStarred() { - return UserData.instance.starredDrugIds?.contains(id) ?? false; + return UserData.instance.starredDrugNames?.contains(name) ?? false; } } diff --git a/app/lib/common/models/userdata/userdata.dart b/app/lib/common/models/userdata/userdata.dart index a00c70046..63440bdd6 100644 --- a/app/lib/common/models/userdata/userdata.dart +++ b/app/lib/common/models/userdata/userdata.dart @@ -40,7 +40,7 @@ class UserData { UserData.instance.lookups?[gene]?.lookupkey; @HiveField(2) - List? starredDrugIds; + List? starredDrugNames; } /// Initializes the user's data by registering all necessary adapters and diff --git a/app/lib/common/pages/drug/cubit.dart b/app/lib/common/pages/drug/cubit.dart index b9b53fb94..acd4d0686 100644 --- a/app/lib/common/pages/drug/cubit.dart +++ b/app/lib/common/pages/drug/cubit.dart @@ -15,12 +15,12 @@ class DrugCubit extends Cubit { final drug = state.whenOrNull(loaded: (drug, _) => drug); if (drug == null) return; - final stars = UserData.instance.starredDrugIds ?? []; + final stars = UserData.instance.starredDrugNames ?? []; if (drug.isStarred()) { - UserData.instance.starredDrugIds = - stars.filter((element) => element != _drug.id).toList(); + UserData.instance.starredDrugNames = + stars.filter((element) => element != _drug.name).toList(); } else { - UserData.instance.starredDrugIds = stars + [_drug.id]; + UserData.instance.starredDrugNames = stars + [_drug.name]; } await UserData.save(); emit(DrugState.loaded(drug, isStarred: drug.isStarred())); From cc293b24d186c82159404a9b22d7db8879d3210e Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Sat, 18 Feb 2023 20:07:40 +0100 Subject: [PATCH 10/35] refactor(#443): starred drugs -> active drugs --- app/integration_test/drugs_test.dart | 6 +++--- app/integration_test/search_test.dart | 2 +- app/lib/common/models/drug/drug.dart | 6 +++--- app/lib/common/models/userdata/userdata.dart | 2 +- app/lib/common/pages/drug/cubit.dart | 18 +++++++++--------- app/lib/common/pages/drug/drug.dart | 10 +++++----- app/lib/common/theme.dart | 4 ++-- app/lib/l10n/app_en.arb | 2 +- app/lib/search/pages/cubit.dart | 12 ++++++------ app/lib/search/pages/search.dart | 8 ++++---- 10 files changed, 35 insertions(+), 35 deletions(-) diff --git a/app/integration_test/drugs_test.dart b/app/integration_test/drugs_test.dart index d4b5fbbf1..e368c3e97 100644 --- a/app/integration_test/drugs_test.dart +++ b/app/integration_test/drugs_test.dart @@ -71,7 +71,7 @@ void main() { brandNames: ['brand name', 'another brand name']), guidelines: [], ); - UserData.instance.starredDrugNames = ['Ibuprofen']; + UserData.instance.activeDrugNames = ['Ibuprofen']; group('integration test for the drugs page', () { testWidgets('test loading', (tester) async { @@ -124,7 +124,7 @@ void main() { testWidgets('test loaded page', (tester) async { when(() => mockDrugsCubit.state) - .thenReturn(DrugState.loaded(testDrug, isStarred: false)); + .thenReturn(DrugState.loaded(testDrug, isActive: false)); late BuildContext context; @@ -188,7 +188,7 @@ void main() { testWidgets('test loaded page without guidelines', (tester) async { when(() => mockDrugsCubit.state).thenReturn( - DrugState.loaded(testDrugWithoutGuidelines, isStarred: true)); + DrugState.loaded(testDrugWithoutGuidelines, isActive: true)); await tester.pumpWidget( MaterialApp( diff --git a/app/integration_test/search_test.dart b/app/integration_test/search_test.dart index fbfb9ece6..22884a99c 100644 --- a/app/integration_test/search_test.dart +++ b/app/integration_test/search_test.dart @@ -8,7 +8,7 @@ import 'package:mocktail/mocktail.dart'; class MockSearchCubit extends MockCubit implements SearchCubit { @override - bool get filterStarred => false; + bool get filterActive => false; } void main() { diff --git a/app/lib/common/models/drug/drug.dart b/app/lib/common/models/drug/drug.dart index b69193d2d..8c949a675 100644 --- a/app/lib/common/models/drug/drug.dart +++ b/app/lib/common/models/drug/drug.dart @@ -66,9 +66,9 @@ class DrugAnnotations { List brandNames; } -extension DrugIsStarred on Drug { - bool isStarred() { - return UserData.instance.starredDrugNames?.contains(name) ?? false; +extension DrugIsActive on Drug { + bool isActive() { + return UserData.instance.activeDrugNames?.contains(name) ?? false; } } diff --git a/app/lib/common/models/userdata/userdata.dart b/app/lib/common/models/userdata/userdata.dart index 63440bdd6..5ef33e0fe 100644 --- a/app/lib/common/models/userdata/userdata.dart +++ b/app/lib/common/models/userdata/userdata.dart @@ -40,7 +40,7 @@ class UserData { UserData.instance.lookups?[gene]?.lookupkey; @HiveField(2) - List? starredDrugNames; + List? activeDrugNames; } /// Initializes the user's data by registering all necessary adapters and diff --git a/app/lib/common/pages/drug/cubit.dart b/app/lib/common/pages/drug/cubit.dart index acd4d0686..edbf7130b 100644 --- a/app/lib/common/pages/drug/cubit.dart +++ b/app/lib/common/pages/drug/cubit.dart @@ -6,24 +6,24 @@ part 'cubit.freezed.dart'; class DrugCubit extends Cubit { DrugCubit(this._drug) : super(DrugState.initial()) { - emit(DrugState.loaded(_drug, isStarred: _drug.isStarred())); + emit(DrugState.loaded(_drug, isActive: _drug.isActive())); } final Drug _drug; - Future toggleStarred() async { + Future toggleActive() async { final drug = state.whenOrNull(loaded: (drug, _) => drug); if (drug == null) return; - final stars = UserData.instance.starredDrugNames ?? []; - if (drug.isStarred()) { - UserData.instance.starredDrugNames = - stars.filter((element) => element != _drug.name).toList(); + final active = UserData.instance.activeDrugNames ?? []; + if (drug.isActive()) { + UserData.instance.activeDrugNames = + active.filter((element) => element != _drug.name).toList(); } else { - UserData.instance.starredDrugNames = stars + [_drug.name]; + UserData.instance.activeDrugNames = active + [_drug.name]; } await UserData.save(); - emit(DrugState.loaded(drug, isStarred: drug.isStarred())); + emit(DrugState.loaded(drug, isActive: drug.isActive())); } } @@ -31,7 +31,7 @@ class DrugCubit extends Cubit { class DrugState with _$DrugState { const factory DrugState.initial() = _InitialState; const factory DrugState.loading() = _LoadingState; - const factory DrugState.loaded(Drug drug, {required bool isStarred}) = + const factory DrugState.loaded(Drug drug, {required bool isActive}) = _LoadedState; const factory DrugState.error() = _ErrorState; } diff --git a/app/lib/common/pages/drug/drug.dart b/app/lib/common/pages/drug/drug.dart index 4fa0861b4..5e51ba9ce 100644 --- a/app/lib/common/pages/drug/drug.dart +++ b/app/lib/common/pages/drug/drug.dart @@ -28,12 +28,12 @@ class DrugPage extends StatelessWidget { body: [errorIndicator(context.l10n.err_generic)]), loading: () => pageScaffold(title: drugName, body: [loadingIndicator()]), - loaded: (drug, isStarred) => pageScaffold( + loaded: (drug, isActive) => pageScaffold( title: drugName, actions: [ IconButton( - onPressed: () => context.read().toggleStarred(), - icon: PharMeTheme.starIcon(isStarred: isStarred), + onPressed: () => context.read().toggleActive(), + icon: PharMeTheme.activeDrugIcon(isActive: isActive), ), IconButton( onPressed: () => sharePdf(drug), @@ -44,7 +44,7 @@ class DrugPage extends StatelessWidget { ) ], body: [ - _buildDrugsPage(drug, isStarred: isStarred, context: context) + _buildDrugsPage(drug, isActive: isActive, context: context) ], ), ); @@ -55,7 +55,7 @@ class DrugPage extends StatelessWidget { Widget _buildDrugsPage( Drug drug, { - required bool isStarred, + required bool isActive, required BuildContext context, }) { final userGuideline = drug.userGuideline(); diff --git a/app/lib/common/theme.dart b/app/lib/common/theme.dart index 31d9c4478..57b7e7d4b 100644 --- a/app/lib/common/theme.dart +++ b/app/lib/common/theme.dart @@ -71,8 +71,8 @@ class PharMeTheme { static const errorColor = Color(0xccf52a2a); static final borderColor = Colors.black.withOpacity(.2); - static Icon starIcon({required bool isStarred, double? size}) { - return Icon(isStarred ? Icons.star_rounded : Icons.star_border_rounded, + static Icon activeDrugIcon({required bool isActive, double? size}) { + return Icon(isActive ? Icons.star_rounded : Icons.star_border_rounded, size: size, color: primaryColor); } } diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 9d01e1f48..ddf6a1cf5 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -6,7 +6,7 @@ "err_could_not_retrieve_access_token": "An unexpected error occurred while retrieving the access token", "err_fetch_user_data_failed": "An unexpected error occurred while fetching your genomic data", "err_generic": "Error!", - "err_no_starred_drugs": "You have no starred drugs! Try disabling the filter by tapping the star icon above.", + "err_no_active_drugs": "You have no active drugs! Try disabling the filter next to the search bar above.", "faq_page_description": "Here you can find answers to common questions about PGx", "faq_pharmacogenomics": "Pharmacogenomics", diff --git a/app/lib/search/pages/cubit.dart b/app/lib/search/pages/cubit.dart index df60421e4..20456d478 100644 --- a/app/lib/search/pages/cubit.dart +++ b/app/lib/search/pages/cubit.dart @@ -14,11 +14,11 @@ class SearchCubit extends Cubit { Timer? searchTimeout; String searchValue = ''; - bool filterStarred = false; + bool filterActive = false; final duration = Duration(milliseconds: 500); - void search({String? query, bool? filterStarred}) { - this.filterStarred = filterStarred ?? this.filterStarred; + void search({String? query, bool? filterActive}) { + this.filterActive = filterActive ?? this.filterActive; searchValue = query ?? searchValue; state.whenOrNull( @@ -27,7 +27,7 @@ class SearchCubit extends Cubit { allDrugs, allDrugs .filter((drug) => - (!this.filterStarred || drug.isStarred()) && + (!this.filterActive || drug.isActive()) && drug.matches(query: searchValue)) .toList())), error: loadDrugs); @@ -54,7 +54,7 @@ class SearchCubit extends Cubit { } void toggleFilter() { - search(filterStarred: !filterStarred); + search(filterActive: !filterActive); } void _emitFilteredLoaded(List drugs) { @@ -62,7 +62,7 @@ class SearchCubit extends Cubit { drugs, drugs .filter((drug) => - (!filterStarred || drug.isStarred()) && + (!filterActive || drug.isActive()) && drug.matches(query: searchValue)) .toList())); } diff --git a/app/lib/search/pages/search.dart b/app/lib/search/pages/search.dart index 259aad0f7..4d9bca472 100644 --- a/app/lib/search/pages/search.dart +++ b/app/lib/search/pages/search.dart @@ -33,8 +33,8 @@ class SearchPage extends HookWidget { TooltipIcon(context.l10n.search_page_tooltip_search), IconButton( onPressed: () => context.read().toggleFilter(), - icon: PharMeTheme.starIcon( - isStarred: context.read().filterStarred)), + icon: PharMeTheme.activeDrugIcon( + isActive: context.read().filterActive)), ]), body: state.when( initial: () => [Container()], @@ -46,8 +46,8 @@ class SearchPage extends HookWidget { } List _buildDrugsList(BuildContext context, List drugs) { - if (drugs.isEmpty && context.read().filterStarred) { - return [errorIndicator(context.l10n.err_no_starred_drugs)]; + if (drugs.isEmpty && context.read().filterActive) { + return [errorIndicator(context.l10n.err_no_active_drugs)]; } return [ SizedBox(height: 8), From e0afbcc48dbfaabf50a4da6def090273920cf5eb Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Sat, 18 Feb 2023 20:50:31 +0100 Subject: [PATCH 11/35] feat(#443): dynamically match guideline with drug inhibitors --- app/lib/common/models/drug/drug_inhibitors.dart | 5 ++++- app/lib/common/models/userdata/userdata.dart | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/app/lib/common/models/drug/drug_inhibitors.dart b/app/lib/common/models/drug/drug_inhibitors.dart index 433814382..2bf92f450 100644 --- a/app/lib/common/models/drug/drug_inhibitors.dart +++ b/app/lib/common/models/drug/drug_inhibitors.dart @@ -1,5 +1,8 @@ // Everything has to match literally. The final value is not a phenotype but -// the CPIC lookupkey value. +// the CPIC lookupkey value. If a user has multiple of the given drugs active, +// the topmost one will be used, i.e. the inhibitors should go from most to +// least severe. + // structure: gene symbol -> drug name -> overwriting lookupkey const Map> drugInhibitors = { diff --git a/app/lib/common/models/userdata/userdata.dart b/app/lib/common/models/userdata/userdata.dart index 5ef33e0fe..dda00db70 100644 --- a/app/lib/common/models/userdata/userdata.dart +++ b/app/lib/common/models/userdata/userdata.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'package:hive/hive.dart'; import '../../../search/module.dart'; @@ -36,8 +37,17 @@ class UserData { @HiveField(1) Map? lookups; - static String? lookupFor(String gene) => - UserData.instance.lookups?[gene]?.lookupkey; + static String? lookupFor(String gene) { + final inhibitors = drugInhibitors[gene]; + if (inhibitors != null) { + final lookup = inhibitors.entries.firstWhereOrNull( + (entry) => + UserData.instance.activeDrugNames?.contains(entry.key) ?? false, + ); + if (lookup != null) return lookup.value; + } + return UserData.instance.lookups?[gene]?.lookupkey; + } @HiveField(2) List? activeDrugNames; From 176b3038a74f3312a557afb764e95442e8cc1abd Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Sat, 18 Feb 2023 21:26:31 +0100 Subject: [PATCH 12/35] fix(#443): lookupkey lookup --- .../common/pages/drug/widgets/annotation_cards/guideline.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/lib/common/pages/drug/widgets/annotation_cards/guideline.dart b/app/lib/common/pages/drug/widgets/annotation_cards/guideline.dart index 2602cf18b..ae3231a5a 100644 --- a/app/lib/common/pages/drug/widgets/annotation_cards/guideline.dart +++ b/app/lib/common/pages/drug/widgets/annotation_cards/guideline.dart @@ -55,8 +55,8 @@ class GuidelineAnnotationCard extends StatelessWidget { } Widget _buildHeader(BuildContext context) { - final geneDescriptions = guideline.lookupkey.keys.map((geneSymbol) => - '$geneSymbol (${UserData.phenotypeFor('geneSymbol')!})'); + final geneDescriptions = guideline.lookupkey.keys.map( + (geneSymbol) => '$geneSymbol (${UserData.phenotypeFor(geneSymbol)!})'); return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ SubHeader(context.l10n.drugs_page_your_genome), SizedBox(height: 12), From 7144d72435fe950b20939799760ff1a46f259237 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Sat, 18 Feb 2023 21:26:56 +0100 Subject: [PATCH 13/35] test(#443): correct testing data --- app/integration_test/drugs_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/integration_test/drugs_test.dart b/app/integration_test/drugs_test.dart index e368c3e97..efcbf0405 100644 --- a/app/integration_test/drugs_test.dart +++ b/app/integration_test/drugs_test.dart @@ -50,7 +50,7 @@ void main() { geneSymbol: 'CYP2C9', phenotype: 'Normal Metabolizer', genotype: '*1/*1', - lookupkey: 'Normal Metabolizer') + lookupkey: '2') }; UserData.instance.diplotypes = { 'CYP2C9': Diplotype( From 181c5f8ec2039553406b02341d327ea4bfdfbc11 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Sat, 18 Feb 2023 21:34:38 +0100 Subject: [PATCH 14/35] fix(app): recommendation text alignment --- .../common/pages/drug/widgets/annotation_cards/guideline.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/common/pages/drug/widgets/annotation_cards/guideline.dart b/app/lib/common/pages/drug/widgets/annotation_cards/guideline.dart index ae3231a5a..eed82dc1d 100644 --- a/app/lib/common/pages/drug/widgets/annotation_cards/guideline.dart +++ b/app/lib/common/pages/drug/widgets/annotation_cards/guideline.dart @@ -34,7 +34,7 @@ class GuidelineAnnotationCard extends StatelessWidget { color: guideline.annotations.warningLevel.color, child: Padding( padding: EdgeInsets.all(12), - child: Column(children: [ + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Icon(guideline.annotations.warningLevel.icon, color: PharMeTheme.onSurfaceText), From bc010d014fa7c1b1c7c6fddd7ade98de45499a85 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Sat, 18 Feb 2023 21:37:27 +0100 Subject: [PATCH 15/35] refactor(#443): make disclaimer reusable --- app/lib/common/pages/drug/widgets/disclaimer.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/lib/common/pages/drug/widgets/disclaimer.dart b/app/lib/common/pages/drug/widgets/disclaimer.dart index b49abdb5f..bff430eab 100644 --- a/app/lib/common/pages/drug/widgets/disclaimer.dart +++ b/app/lib/common/pages/drug/widgets/disclaimer.dart @@ -4,7 +4,9 @@ import '../../../l10n.dart'; import '../../../theme.dart'; class Disclaimer extends StatelessWidget { - const Disclaimer(); + const Disclaimer({this.text}); + + final String? text; @override Widget build(BuildContext context) { @@ -24,7 +26,7 @@ class Disclaimer extends StatelessWidget { SizedBox(width: 8), Flexible( child: Text( - context.l10n.drugs_page_disclaimer, + text ?? context.l10n.drugs_page_disclaimer, style: PharMeTheme.textTheme.labelMedium!.copyWith( fontWeight: FontWeight.w100, ), From 001922ab32292fc87a72462f3b062e0386a10cfa Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Sat, 18 Feb 2023 22:02:26 +0100 Subject: [PATCH 16/35] feat(#443): add hint about phenotype being adjusted --- app/lib/common/models/userdata/userdata.dart | 20 ++++++++++++------- .../widgets/annotation_cards/guideline.dart | 16 +++++++++++---- app/lib/l10n/app_en.arb | 10 ++++++++++ 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/app/lib/common/models/userdata/userdata.dart b/app/lib/common/models/userdata/userdata.dart index dda00db70..54e4802f3 100644 --- a/app/lib/common/models/userdata/userdata.dart +++ b/app/lib/common/models/userdata/userdata.dart @@ -37,14 +37,20 @@ class UserData { @HiveField(1) Map? lookups; - static String? lookupFor(String gene) { + + static MapEntry? overwrittenLookup(String gene) { final inhibitors = drugInhibitors[gene]; - if (inhibitors != null) { - final lookup = inhibitors.entries.firstWhereOrNull( - (entry) => - UserData.instance.activeDrugNames?.contains(entry.key) ?? false, - ); - if (lookup != null) return lookup.value; + if (inhibitors == null) return null; + final lookup = inhibitors.entries.firstWhereOrNull((entry) => + UserData.instance.activeDrugNames?.contains(entry.key) ?? false); + if (lookup == null) return null; + return lookup; + } + + static String? lookupFor(String gene) { + final overwrittenLookup = UserData.overwrittenLookup(gene); + if (overwrittenLookup != null) { + return overwrittenLookup.value; } return UserData.instance.lookups?[gene]?.lookupkey; } diff --git a/app/lib/common/pages/drug/widgets/annotation_cards/guideline.dart b/app/lib/common/pages/drug/widgets/annotation_cards/guideline.dart index eed82dc1d..35bb5a42c 100644 --- a/app/lib/common/pages/drug/widgets/annotation_cards/guideline.dart +++ b/app/lib/common/pages/drug/widgets/annotation_cards/guideline.dart @@ -34,7 +34,8 @@ class GuidelineAnnotationCard extends StatelessWidget { color: guideline.annotations.warningLevel.color, child: Padding( padding: EdgeInsets.all(12), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + child: + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Icon(guideline.annotations.warningLevel.icon, color: PharMeTheme.onSurfaceText), @@ -55,15 +56,22 @@ class GuidelineAnnotationCard extends StatelessWidget { } Widget _buildHeader(BuildContext context) { - final geneDescriptions = guideline.lookupkey.keys.map( - (geneSymbol) => '$geneSymbol (${UserData.phenotypeFor(geneSymbol)!})'); + final geneDescriptions = guideline.lookupkey.keys.map((geneSymbol) { + final overwritingDrug = UserData.overwrittenLookup(geneSymbol)?.key; + final hint = overwritingDrug.isNotNullOrEmpty + ? '(${context.l10n.drugs_page_overwritten_phenotype(overwritingDrug!)})' + : ''; + final genePhenotype = + '$geneSymbol: ${UserData.phenotypeFor(geneSymbol)!}'; + return [genePhenotype, hint].join(' '); + }); return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ SubHeader(context.l10n.drugs_page_your_genome), SizedBox(height: 12), Text( geneDescriptions.join(', '), style: PharMeTheme.textTheme.bodyLarge!, - ) + ), ]); } diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index ddf6a1cf5..dec0f2ea8 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -19,6 +19,16 @@ "search_page_tooltip_search": "Search for drugs by their name, brand name or class.", "drugs_page_disclaimer": "This assessment is ONLY based on your genetics. Important factors like weight, age and pre-existing conditions are not considered.", + "drugs_page_overwritten_phenotype": "adjusted based on you taking {drugName}", + "@drugs_page_overwritten_phenotype": { + "description": "Disclaimer for when the phenotype has been adjusted based on an active drug", + "placeholders": { + "drugName": { + "type": "String", + "example": "bupropion" + } + } + }, "drugs_page_your_genome": "Your genome", "drugs_page_header_further_info": "Further Information", "drugs_page_header_synonyms": "Synonyms", From f9fd84d9b1cebf7a83f19e743318c9d7308c3636 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 09:53:56 +0100 Subject: [PATCH 17/35] feat(#479): checkbox for activity --- app/lib/common/pages/drug/cubit.dart | 12 +++++++----- app/lib/common/pages/drug/drug.dart | 12 ++++++------ .../drug/widgets/annotation_cards/drug.dart | 19 ++++++++++++++++++- app/lib/l10n/app_en.arb | 5 ++++- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/app/lib/common/pages/drug/cubit.dart b/app/lib/common/pages/drug/cubit.dart index edbf7130b..8f3a67b4c 100644 --- a/app/lib/common/pages/drug/cubit.dart +++ b/app/lib/common/pages/drug/cubit.dart @@ -11,19 +11,21 @@ class DrugCubit extends Cubit { final Drug _drug; - Future toggleActive() async { + // ignore: avoid_positional_boolean_parameters + Future setActivity(bool? value) async { + if (value == null) return; final drug = state.whenOrNull(loaded: (drug, _) => drug); if (drug == null) return; final active = UserData.instance.activeDrugNames ?? []; - if (drug.isActive()) { + if (value) { + UserData.instance.activeDrugNames = active + [_drug.name]; + } else { UserData.instance.activeDrugNames = active.filter((element) => element != _drug.name).toList(); - } else { - UserData.instance.activeDrugNames = active + [_drug.name]; } await UserData.save(); - emit(DrugState.loaded(drug, isActive: drug.isActive())); + emit(DrugState.loaded(drug, isActive: value)); } } diff --git a/app/lib/common/pages/drug/drug.dart b/app/lib/common/pages/drug/drug.dart index 5e51ba9ce..d0a15b808 100644 --- a/app/lib/common/pages/drug/drug.dart +++ b/app/lib/common/pages/drug/drug.dart @@ -31,10 +31,6 @@ class DrugPage extends StatelessWidget { loaded: (drug, isActive) => pageScaffold( title: drugName, actions: [ - IconButton( - onPressed: () => context.read().toggleActive(), - icon: PharMeTheme.activeDrugIcon(isActive: isActive), - ), IconButton( onPressed: () => sharePdf(drug), icon: Icon( @@ -64,9 +60,13 @@ class DrugPage extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SubHeader(context.l10n.drugs_page_header_druginfo), + SubHeader(context.l10n.drugs_page_header_drug), SizedBox(height: 12), - DrugAnnotationCard(drug), + DrugAnnotationCard( + drug, + isActive: isActive, + setActivity: context.read().setActivity, + ), SizedBox(height: 20), SubHeader( context.l10n.drugs_page_header_guideline, diff --git a/app/lib/common/pages/drug/widgets/annotation_cards/drug.dart b/app/lib/common/pages/drug/widgets/annotation_cards/drug.dart index 91f71d106..ae4ca6bdb 100644 --- a/app/lib/common/pages/drug/widgets/annotation_cards/drug.dart +++ b/app/lib/common/pages/drug/widgets/annotation_cards/drug.dart @@ -1,9 +1,16 @@ import '../../../../module.dart'; +import '../sub_header.dart'; class DrugAnnotationCard extends StatelessWidget { - const DrugAnnotationCard(this.drug); + const DrugAnnotationCard( + this.drug, { + required this.isActive, + required this.setActivity, + }); final Drug drug; + final bool isActive; + final void Function(bool?) setActivity; @override Widget build(BuildContext context) { @@ -12,6 +19,8 @@ class DrugAnnotationCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + SubHeader(context.l10n.drugs_page_header_druginfo), + SizedBox(height: 12), Text(drug.annotations.indication), SizedBox(height: 8), Table(defaultColumnWidth: IntrinsicColumnWidth(), children: [ @@ -22,6 +31,14 @@ class DrugAnnotationCard extends StatelessWidget { drug.annotations.brandNames.join(', ')), ] ]), + SizedBox(height: 12), + SubHeader(context.l10n.drugs_page_header_active), + CheckboxListTile( + title: Text(context.l10n.drugs_page_active), + value: isActive, + onChanged: setActivity, + controlAffinity: ListTileControlAffinity.leading, + ), ], ), ), diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index dec0f2ea8..fd74bcd68 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -33,7 +33,10 @@ "drugs_page_header_further_info": "Further Information", "drugs_page_header_synonyms": "Synonyms", "drugs_page_header_drugclass": "Drug class", - "drugs_page_header_druginfo": "Drug Information", + "drugs_page_header_drug": "Drug", + "drugs_page_header_druginfo": "Information", + "drugs_page_header_active": "Activity", + "drugs_page_active": "I am actively taking this drug.", "drugs_page_header_guideline": "Clinical Guideline", "drugs_page_header_recommendation": "Recommendation", "drugs_page_no_guidelines_for_phenotype": "We couldn't find a guideline matching your genetics.", From 19b9facda1d9053fe8d12cc15eddb36ba7146934 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 09:56:47 +0100 Subject: [PATCH 18/35] feat(#479): add section dividers --- app/lib/common/pages/drug/widgets/annotation_cards/drug.dart | 4 +++- .../common/pages/drug/widgets/annotation_cards/guideline.dart | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/lib/common/pages/drug/widgets/annotation_cards/drug.dart b/app/lib/common/pages/drug/widgets/annotation_cards/drug.dart index ae4ca6bdb..d8e009e05 100644 --- a/app/lib/common/pages/drug/widgets/annotation_cards/drug.dart +++ b/app/lib/common/pages/drug/widgets/annotation_cards/drug.dart @@ -31,7 +31,9 @@ class DrugAnnotationCard extends StatelessWidget { drug.annotations.brandNames.join(', ')), ] ]), - SizedBox(height: 12), + SizedBox(height: 4), + Divider(color: PharMeTheme.borderColor), + SizedBox(height: 4), SubHeader(context.l10n.drugs_page_header_active), CheckboxListTile( title: Text(context.l10n.drugs_page_active), diff --git a/app/lib/common/pages/drug/widgets/annotation_cards/guideline.dart b/app/lib/common/pages/drug/widgets/annotation_cards/guideline.dart index 35bb5a42c..1cf0af79f 100644 --- a/app/lib/common/pages/drug/widgets/annotation_cards/guideline.dart +++ b/app/lib/common/pages/drug/widgets/annotation_cards/guideline.dart @@ -17,7 +17,9 @@ class GuidelineAnnotationCard extends StatelessWidget { _buildHeader(context), SizedBox(height: 12), _buildCard(context), - SizedBox(height: 16), + SizedBox(height: 8), + Divider(color: PharMeTheme.borderColor), + SizedBox(height: 8), _buildSourcesSection(context), SizedBox(height: 12), ]), From 3627ab739b050bd0748c24e42bc670b7f4ccef93 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 10:21:52 +0100 Subject: [PATCH 19/35] feat(#479): avoid duplicate active drugs --- app/lib/common/models/userdata/userdata.dart | 1 + app/lib/common/pages/drug/cubit.dart | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/lib/common/models/userdata/userdata.dart b/app/lib/common/models/userdata/userdata.dart index 54e4802f3..b1d0158e0 100644 --- a/app/lib/common/models/userdata/userdata.dart +++ b/app/lib/common/models/userdata/userdata.dart @@ -55,6 +55,7 @@ class UserData { return UserData.instance.lookups?[gene]?.lookupkey; } + // hive can't deal with sets so we have to use a list :( @HiveField(2) List? activeDrugNames; } diff --git a/app/lib/common/pages/drug/cubit.dart b/app/lib/common/pages/drug/cubit.dart index 8f3a67b4c..42a33e61b 100644 --- a/app/lib/common/pages/drug/cubit.dart +++ b/app/lib/common/pages/drug/cubit.dart @@ -17,13 +17,13 @@ class DrugCubit extends Cubit { final drug = state.whenOrNull(loaded: (drug, _) => drug); if (drug == null) return; - final active = UserData.instance.activeDrugNames ?? []; + final active = (UserData.instance.activeDrugNames ?? []) + .filter((name) => name != _drug.name) + .toList(); if (value) { - UserData.instance.activeDrugNames = active + [_drug.name]; - } else { - UserData.instance.activeDrugNames = - active.filter((element) => element != _drug.name).toList(); + active.add(_drug.name); } + UserData.instance.activeDrugNames = active; await UserData.save(); emit(DrugState.loaded(drug, isActive: value)); } From 408ea53b02e7fb2a742ab791f51ebb2427a0f959 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 10:46:19 +0100 Subject: [PATCH 20/35] feat(#479): show activity change warning --- .../drug/widgets/annotation_cards/drug.dart | 26 ++++++++++++++++++- app/lib/l10n/app_en.arb | 7 +++-- app/lib/settings/pages/settings.dart | 4 +-- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/app/lib/common/pages/drug/widgets/annotation_cards/drug.dart b/app/lib/common/pages/drug/widgets/annotation_cards/drug.dart index d8e009e05..13b702dde 100644 --- a/app/lib/common/pages/drug/widgets/annotation_cards/drug.dart +++ b/app/lib/common/pages/drug/widgets/annotation_cards/drug.dart @@ -1,3 +1,5 @@ +import 'package:flutter/cupertino.dart'; + import '../../../../module.dart'; import '../sub_header.dart'; @@ -36,9 +38,31 @@ class DrugAnnotationCard extends StatelessWidget { SizedBox(height: 4), SubHeader(context.l10n.drugs_page_header_active), CheckboxListTile( + activeColor: PharMeTheme.primaryColor, title: Text(context.l10n.drugs_page_active), value: isActive, - onChanged: setActivity, + onChanged: (newValue) => showCupertinoModalPopup( + context: context, + builder: (context) => CupertinoAlertDialog( + title: Text(context.l10n.drugs_page_active_warn_header), + content: Text(context.l10n.drugs_page_active_warn), + actions: [ + CupertinoDialogAction( + isDefaultAction: true, + onPressed: () => Navigator.pop(context, 'Cancel'), + child: Text(context.l10n.action_cancel), + ), + CupertinoDialogAction( + isDestructiveAction: true, + onPressed: () { + Navigator.pop(context, 'OK'); + setActivity(newValue); + }, + child: Text(context.l10n.action_continue), + ), + ], + ), + ), controlAffinity: ListTileControlAffinity.leading, ), ], diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index fd74bcd68..09b0877d1 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -1,4 +1,7 @@ { + "action_cancel": "Cancel", + "action_continue": "Continue", + "auth_choose_lab": "Please select your lab", "auth_sign_in": "Sign in", "auth_success": "Successfully imported data", @@ -37,6 +40,8 @@ "drugs_page_header_druginfo": "Information", "drugs_page_header_active": "Activity", "drugs_page_active": "I am actively taking this drug.", + "drugs_page_active_warn_header": "Are you sure you want to change the activity status?", + "drugs_page_active_warn": "This may adjust the guidelines you receive for other drugs.", "drugs_page_header_guideline": "Clinical Guideline", "drugs_page_header_recommendation": "Recommendation", "drugs_page_no_guidelines_for_phenotype": "We couldn't find a guideline matching your genetics.", @@ -108,8 +113,6 @@ "onboarding_5_text": "After signing in to the lab, your genetic data is sent straight onto your phone.\n\nOur servers know nothing about you, neither your identity nor your genetics.", "settings_page_account_settings": "Account Settings", - "settings_page_cancel": "Cancel", - "settings_page_continue": "Continue", "settings_page_delete_data": "Delete App Data", "settings_page_delete_data_text": "Are you sure that you want to delete all app data? This also includes your genomic data.", "settings_page_more": "More", diff --git a/app/lib/settings/pages/settings.dart b/app/lib/settings/pages/settings.dart index 08ce86751..7cbbef48c 100644 --- a/app/lib/settings/pages/settings.dart +++ b/app/lib/settings/pages/settings.dart @@ -63,7 +63,7 @@ class SettingsPage extends StatelessWidget { actions: [ TextButton( onPressed: context.router.root.pop, - child: Text(context.l10n.settings_page_cancel), + child: Text(context.l10n.action_cancel), ), TextButton( onPressed: () async { @@ -71,7 +71,7 @@ class SettingsPage extends StatelessWidget { await context.router.replaceAll([LoginRouter()]); }, child: Text( - context.l10n.settings_page_continue, + context.l10n.action_continue, style: TextStyle(color: PharMeTheme.errorColor), ), ), From 09b16adc6fb6690565c3fc315f4ca62e9355c39b Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 10:56:43 +0100 Subject: [PATCH 21/35] feat(#479): remove star icon --- app/lib/common/theme.dart | 5 ----- app/lib/search/pages/search.dart | 3 +-- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/app/lib/common/theme.dart b/app/lib/common/theme.dart index 57b7e7d4b..c8afc6ac7 100644 --- a/app/lib/common/theme.dart +++ b/app/lib/common/theme.dart @@ -70,11 +70,6 @@ class PharMeTheme { static const backgroundColor = Colors.white; static const errorColor = Color(0xccf52a2a); static final borderColor = Colors.black.withOpacity(.2); - - static Icon activeDrugIcon({required bool isActive, double? size}) { - return Icon(isActive ? Icons.star_rounded : Icons.star_border_rounded, - size: size, color: primaryColor); - } } extension WarningLevelColor on WarningLevel { diff --git a/app/lib/search/pages/search.dart b/app/lib/search/pages/search.dart index 4d9bca472..1aa462910 100644 --- a/app/lib/search/pages/search.dart +++ b/app/lib/search/pages/search.dart @@ -33,8 +33,7 @@ class SearchPage extends HookWidget { TooltipIcon(context.l10n.search_page_tooltip_search), IconButton( onPressed: () => context.read().toggleFilter(), - icon: PharMeTheme.activeDrugIcon( - isActive: context.read().filterActive)), + icon: Icon(Icons.filter_list_rounded)), ]), body: state.when( initial: () => [Container()], From 3601e4bd0d9b4750d951b597abf3ad80c6b0d6ac Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 12:01:36 +0100 Subject: [PATCH 22/35] dev(#479): add popover dep --- app/pubspec.lock | 11 +++++++++-- app/pubspec.yaml | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/pubspec.lock b/app/pubspec.lock index 3da36244f..fc2038de5 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -834,6 +834,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.5.1" + popover: + dependency: "direct main" + description: + name: popover + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.8+1" process: dependency: transitive description: @@ -1246,5 +1253,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=2.17.0 <3.0.0" - flutter: ">=3.0.0" + dart: ">=2.18.0 <3.0.0" + flutter: ">=3.3.0" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 97d1b7f67..ac7aae1c5 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -36,6 +36,7 @@ dependencies: intl: ^0.17.0 json_annotation: ^4.6.0 path_provider: ^2.0.11 + popover: ^0.2.8+1 pdf: ^3.8.1 shared_preferences: ^2.0.15 url_launcher: ^6.1.4 From e7cfa4fc6f4158e816e4a1f28b2ae2726d7004d2 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 13:14:04 +0100 Subject: [PATCH 23/35] feat(#479): popover context menu --- app/lib/common/widgets/context_menu.dart | 87 ++++++++++++++++++++++++ app/lib/common/widgets/module.dart | 1 + 2 files changed, 88 insertions(+) create mode 100644 app/lib/common/widgets/context_menu.dart diff --git a/app/lib/common/widgets/context_menu.dart b/app/lib/common/widgets/context_menu.dart new file mode 100644 index 000000000..b1e1d6412 --- /dev/null +++ b/app/lib/common/widgets/context_menu.dart @@ -0,0 +1,87 @@ +import 'package:popover/popover.dart'; + +import '../module.dart'; + +class ContextMenu extends StatelessWidget { + const ContextMenu({ + super.key, + required this.items, + required this.child, + }); + + final List items; + final Widget child; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + showPopover( + context: context, + bodyBuilder: (context) => Padding( + padding: EdgeInsets.symmetric(horizontal: 12), + child: Container( + decoration: BoxDecoration( + color: PharMeTheme.onSurfaceColor, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: items + .mapIndexed((index, item) => (index == items.count() - 1) + ? item + : Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + width: 0.5, color: PharMeTheme.borderColor), + )), + child: item)) + .toList(), + ), + ), + ), + direction: PopoverDirection.bottom, + arrowHeight: 0, + arrowWidth: 0, + transitionDuration: Duration(milliseconds: 100), + barrierColor: Color.fromRGBO(0, 0, 0, 0.05), + backgroundColor: Color.fromRGBO(1, 1, 1, 0), + shadow: [], + ); + }, + child: child); + } +} + +class ContextMenuAction extends StatelessWidget { + const ContextMenuAction({ + super.key, + required this.label, + required this.action, + this.icon, + }); + + final String label; + final void Function() action; + final IconData? icon; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: action, + child: Padding( + padding: EdgeInsets.all(12), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (icon != null) Icon(icon!, size: 24) else SizedBox(width: 24), + SizedBox(width: 8), + Text(label), + ], + ), + ), + ); + } +} diff --git a/app/lib/common/widgets/module.dart b/app/lib/common/widgets/module.dart index 2cbbfd4ef..75550056b 100644 --- a/app/lib/common/widgets/module.dart +++ b/app/lib/common/widgets/module.dart @@ -1,4 +1,5 @@ export 'app.dart'; +export 'context_menu.dart'; export 'headings.dart'; export 'indicators.dart'; export 'page_scaffold.dart'; From 2e966c18efe8b767b0d644c8262fc3c15072f13e Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 15:16:56 +0100 Subject: [PATCH 24/35] feat(#479): add 'none' warning level --- app/lib/common/models/drug/warning_level.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/lib/common/models/drug/warning_level.dart b/app/lib/common/models/drug/warning_level.dart index 1b7726ec0..600dfe6ad 100644 --- a/app/lib/common/models/drug/warning_level.dart +++ b/app/lib/common/models/drug/warning_level.dart @@ -10,7 +10,9 @@ enum WarningLevel { @HiveField(1) yellow, @HiveField(2) - green + green, + @HiveField(3) + none, } extension WarningLevelIcon on WarningLevel { From ed791de5d441628ba1417d4f83cc62547f811e9e Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 15:17:22 +0100 Subject: [PATCH 25/35] feat(#479): search filtering --- app/lib/search/pages/cubit.dart | 92 ++++++++++++++++++++++---------- app/lib/search/pages/search.dart | 39 +++++++++++--- 2 files changed, 96 insertions(+), 35 deletions(-) diff --git a/app/lib/search/pages/cubit.dart b/app/lib/search/pages/cubit.dart index 20456d478..ab374d611 100644 --- a/app/lib/search/pages/cubit.dart +++ b/app/lib/search/pages/cubit.dart @@ -14,29 +14,34 @@ class SearchCubit extends Cubit { Timer? searchTimeout; String searchValue = ''; - bool filterActive = false; final duration = Duration(milliseconds: 500); - void search({String? query, bool? filterActive}) { - this.filterActive = filterActive ?? this.filterActive; - searchValue = query ?? searchValue; - + void search({ + String? query, + bool? toggleInactive, + WarningLevel? toggleWarningLevel, + }) { state.whenOrNull( - initial: loadDrugs, - loaded: (allDrugs, _) => emit(SearchState.loaded( - allDrugs, - allDrugs - .filter((drug) => - (!this.filterActive || drug.isActive()) && - drug.matches(query: searchValue)) - .toList())), - error: loadDrugs); + initial: loadDrugs, + loaded: (allDrugs, filter) => emit( + SearchState.loaded( + allDrugs, + FilterState.from( + filter, + query: query, + toggleInactive: toggleInactive, + toggleWarningLevel: toggleWarningLevel, + ), + ), + ), + error: loadDrugs, + ); } Future loadDrugs({bool updateIfNull = true}) async { final drugs = CachedDrugs.instance.drugs; if (drugs != null) { - _emitFilteredLoaded(drugs); + emit(SearchState.loaded(drugs, FilterState.initial())); return; } if (!updateIfNull) { @@ -53,19 +58,50 @@ class SearchCubit extends Cubit { } } - void toggleFilter() { - search(filterActive: !filterActive); - } + FilterState? get filter => state.whenOrNull(loaded: (_, filter) => filter); +} + +class FilterState { + FilterState({ + required this.query, + required this.showInactive, + required this.showWarningLevel, + }); - void _emitFilteredLoaded(List drugs) { - emit(SearchState.loaded( - drugs, - drugs - .filter((drug) => - (!filterActive || drug.isActive()) && - drug.matches(query: searchValue)) - .toList())); + FilterState.initial() + : this(query: '', showInactive: true, showWarningLevel: { + for (var level in WarningLevel.values) level: true + }); + + FilterState.from( + FilterState other, { + String? query, + bool? toggleInactive, + WarningLevel? toggleWarningLevel, + }) : this( + query: query ?? other.query, + showInactive: (toggleInactive == true) + ? !other.showInactive + : other.showInactive, + showWarningLevel: { + for (var entry in other.showWarningLevel.entries) + entry.key: + (entry.key == toggleWarningLevel) ? !entry.value : entry.value + }, + ); + + final String query; + final bool showInactive; + Map showWarningLevel; + + bool isAccepted(Drug drug) { + final warningLevel = drug.userGuideline()?.annotations.warningLevel; + return drug.matches(query: query) && + (drug.isActive() || showInactive) && + showWarningLevel[warningLevel]!; } + + List filter(List drugs) => drugs.filter(isAccepted).toList(); } @freezed @@ -73,6 +109,8 @@ class SearchState with _$SearchState { const factory SearchState.initial() = _InitialState; const factory SearchState.loading() = _LoadingState; const factory SearchState.loaded( - List allDrugs, List filteredDrugs) = _LoadedState; + List allDrugs, + FilterState filter, + ) = _LoadedState; const factory SearchState.error() = _ErrorState; } diff --git a/app/lib/search/pages/search.dart b/app/lib/search/pages/search.dart index 1aa462910..3f4f47785 100644 --- a/app/lib/search/pages/search.dart +++ b/app/lib/search/pages/search.dart @@ -31,26 +31,49 @@ class SearchPage extends HookWidget { )), SizedBox(width: 12), TooltipIcon(context.l10n.search_page_tooltip_search), - IconButton( - onPressed: () => context.read().toggleFilter(), - icon: Icon(Icons.filter_list_rounded)), + buildFilter(context.read()), ]), body: state.when( initial: () => [Container()], error: () => [errorIndicator(context.l10n.err_generic)], - loaded: (_, drugs) => _buildDrugsList(context, drugs), + loaded: (drugs, filter) => + _buildDrugsList(context, drugs, filter), loading: () => [loadingIndicator()], )); })); } - List _buildDrugsList(BuildContext context, List drugs) { - if (drugs.isEmpty && context.read().filterActive) { - return [errorIndicator(context.l10n.err_no_active_drugs)]; + Widget buildFilter(SearchCubit cubit) { + final filter = cubit.filter; + print('build filter'); + return ContextMenu( + items: [ + ContextMenuCheckmark( + label: 'show inactive', + setState: () => cubit.search(toggleInactive: true), + icon: _checkmark(isEnabled: filter?.showInactive)), + ...WarningLevel.values.map((level) => ContextMenuCheckmark( + label: 'placeholder', + setState: () => cubit.search(toggleWarningLevel: level), + icon: _checkmark(isEnabled: filter?.showWarningLevel[level]))) + ], + child: Padding( + padding: EdgeInsets.all(8), child: Icon(Icons.filter_list_rounded)), + ); + } + + IconData? _checkmark({required bool? isEnabled}) => + isEnabled ?? false ? Icons.check_rounded : null; + + List _buildDrugsList( + BuildContext context, List drugs, FilterState filter) { + final filteredDrugs = filter.filter(drugs); + if (filteredDrugs.isEmpty) { + return [errorIndicator(context.l10n.err_no_drugs)]; } return [ SizedBox(height: 8), - ...drugs.map((drug) => Column(children: [ + ...filteredDrugs.map((drug) => Column(children: [ Padding( padding: EdgeInsets.symmetric(horizontal: 8), child: DrugCard( From b8a7e9c60c823d6861014fbd171757db0a3c17f0 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 15:25:46 +0100 Subject: [PATCH 26/35] feat(#479): no drugs error text --- app/lib/l10n/app_en.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 09b0877d1..677c6a549 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -9,7 +9,7 @@ "err_could_not_retrieve_access_token": "An unexpected error occurred while retrieving the access token", "err_fetch_user_data_failed": "An unexpected error occurred while fetching your genomic data", "err_generic": "Error!", - "err_no_active_drugs": "You have no active drugs! Try disabling the filter next to the search bar above.", + "err_no_drugs": "Try adjusting the filters next to the search bar above.", "faq_page_description": "Here you can find answers to common questions about PGx", "faq_pharmacogenomics": "Pharmacogenomics", From 6a8d6eb26fcb6f3b2331212b9d365f1413058d44 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 15:26:29 +0100 Subject: [PATCH 27/35] feat(#479): refresh checkmark icons --- app/lib/common/widgets/context_menu.dart | 51 ++++++++++++++---------- app/lib/search/pages/cubit.dart | 23 +++++------ app/lib/search/pages/search.dart | 12 ++---- 3 files changed, 44 insertions(+), 42 deletions(-) diff --git a/app/lib/common/widgets/context_menu.dart b/app/lib/common/widgets/context_menu.dart index b1e1d6412..e2bc9d782 100644 --- a/app/lib/common/widgets/context_menu.dart +++ b/app/lib/common/widgets/context_menu.dart @@ -9,7 +9,7 @@ class ContextMenu extends StatelessWidget { required this.child, }); - final List items; + final List items; final Widget child; @override @@ -55,31 +55,40 @@ class ContextMenu extends StatelessWidget { } } -class ContextMenuAction extends StatelessWidget { - const ContextMenuAction({ - super.key, - required this.label, - required this.action, - this.icon, - }); +class ContextMenuCheckmark extends StatelessWidget { + ContextMenuCheckmark( + {super.key, + required this.label, + required this.setState, + this.state = false}); final String label; - final void Function() action; - final IconData? icon; + final void Function(bool) setState; + bool state; @override Widget build(BuildContext context) { - return GestureDetector( - onTap: action, - child: Padding( - padding: EdgeInsets.all(12), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (icon != null) Icon(icon!, size: 24) else SizedBox(width: 24), - SizedBox(width: 8), - Text(label), - ], + return StatefulBuilder( + builder: (context, rebuild) => GestureDetector( + onTap: () { + rebuild(() { + state = !state; + setState(state); + }); + }, + child: Padding( + padding: EdgeInsets.all(12), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (state) + Icon(Icons.check_rounded, size: 24) + else + SizedBox(width: 24), + SizedBox(width: 8), + Text(label), + ], + ), ), ), ); diff --git a/app/lib/search/pages/cubit.dart b/app/lib/search/pages/cubit.dart index ab374d611..58b73c8d2 100644 --- a/app/lib/search/pages/cubit.dart +++ b/app/lib/search/pages/cubit.dart @@ -18,8 +18,8 @@ class SearchCubit extends Cubit { void search({ String? query, - bool? toggleInactive, - WarningLevel? toggleWarningLevel, + bool? showInactive, + Map? showWarningLevel, }) { state.whenOrNull( initial: loadDrugs, @@ -29,8 +29,8 @@ class SearchCubit extends Cubit { FilterState.from( filter, query: query, - toggleInactive: toggleInactive, - toggleWarningLevel: toggleWarningLevel, + showInactive: showInactive, + showWarningLevel: showWarningLevel, ), ), ), @@ -76,23 +76,20 @@ class FilterState { FilterState.from( FilterState other, { String? query, - bool? toggleInactive, - WarningLevel? toggleWarningLevel, + bool? showInactive, + Map? showWarningLevel, }) : this( query: query ?? other.query, - showInactive: (toggleInactive == true) - ? !other.showInactive - : other.showInactive, + showInactive: showInactive ?? other.showInactive, showWarningLevel: { - for (var entry in other.showWarningLevel.entries) - entry.key: - (entry.key == toggleWarningLevel) ? !entry.value : entry.value + for (var level in WarningLevel.values) + level: showWarningLevel?[level] ?? other.showWarningLevel[level]! }, ); final String query; final bool showInactive; - Map showWarningLevel; + final Map showWarningLevel; bool isAccepted(Drug drug) { final warningLevel = drug.userGuideline()?.annotations.warningLevel; diff --git a/app/lib/search/pages/search.dart b/app/lib/search/pages/search.dart index 3f4f47785..8c73a072a 100644 --- a/app/lib/search/pages/search.dart +++ b/app/lib/search/pages/search.dart @@ -45,26 +45,22 @@ class SearchPage extends HookWidget { Widget buildFilter(SearchCubit cubit) { final filter = cubit.filter; - print('build filter'); return ContextMenu( items: [ ContextMenuCheckmark( label: 'show inactive', - setState: () => cubit.search(toggleInactive: true), - icon: _checkmark(isEnabled: filter?.showInactive)), + setState: (state) => cubit.search(showInactive: state), + state: filter?.showInactive ?? false), ...WarningLevel.values.map((level) => ContextMenuCheckmark( label: 'placeholder', - setState: () => cubit.search(toggleWarningLevel: level), - icon: _checkmark(isEnabled: filter?.showWarningLevel[level]))) + setState: (state) => cubit.search(showWarningLevel: {level: state}), + state: filter?.showWarningLevel[level] ?? false)) ], child: Padding( padding: EdgeInsets.all(8), child: Icon(Icons.filter_list_rounded)), ); } - IconData? _checkmark({required bool? isEnabled}) => - isEnabled ?? false ? Icons.check_rounded : null; - List _buildDrugsList( BuildContext context, List drugs, FilterState filter) { final filteredDrugs = filter.filter(drugs); From f42c7f73f88e0f281a46093c8f1525022583fd6f Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 15:27:34 +0100 Subject: [PATCH 28/35] feat(#479): adjust icons --- app/lib/common/widgets/context_menu.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/lib/common/widgets/context_menu.dart b/app/lib/common/widgets/context_menu.dart index e2bc9d782..c84548db2 100644 --- a/app/lib/common/widgets/context_menu.dart +++ b/app/lib/common/widgets/context_menu.dart @@ -82,9 +82,9 @@ class ContextMenuCheckmark extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ if (state) - Icon(Icons.check_rounded, size: 24) + Icon(Icons.check_rounded, size: 16) else - SizedBox(width: 24), + SizedBox(width: 16, height: 16), SizedBox(width: 8), Text(label), ], From 972147bd2561a48ca52f0a01c54974deab218e3e Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 15:38:15 +0100 Subject: [PATCH 29/35] feat(#479): filter labels --- app/lib/l10n/app_en.arb | 5 +++++ app/lib/search/pages/search.dart | 14 ++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 677c6a549..ca32974a9 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -20,6 +20,11 @@ "general_retry": "Retry", "search_page_tooltip_search": "Search for drugs by their name, brand name or class.", + "search_page_filter_inactive": "Inactive drugs", + "search_page_filter_green": "Green warning level", + "search_page_filter_yellow": "Yellow warning level", + "search_page_filter_red": "Red warning level", + "search_page_filter_gray": "No warning level", "drugs_page_disclaimer": "This assessment is ONLY based on your genetics. Important factors like weight, age and pre-existing conditions are not considered.", "drugs_page_overwritten_phenotype": "adjusted based on you taking {drugName}", diff --git a/app/lib/search/pages/search.dart b/app/lib/search/pages/search.dart index 8c73a072a..5798ebec8 100644 --- a/app/lib/search/pages/search.dart +++ b/app/lib/search/pages/search.dart @@ -31,7 +31,7 @@ class SearchPage extends HookWidget { )), SizedBox(width: 12), TooltipIcon(context.l10n.search_page_tooltip_search), - buildFilter(context.read()), + buildFilter(context), ]), body: state.when( initial: () => [Container()], @@ -43,16 +43,22 @@ class SearchPage extends HookWidget { })); } - Widget buildFilter(SearchCubit cubit) { + Widget buildFilter(BuildContext context) { + final cubit = context.read(); final filter = cubit.filter; return ContextMenu( items: [ ContextMenuCheckmark( - label: 'show inactive', + label: context.l10n.search_page_filter_inactive, setState: (state) => cubit.search(showInactive: state), state: filter?.showInactive ?? false), ...WarningLevel.values.map((level) => ContextMenuCheckmark( - label: 'placeholder', + label: { + WarningLevel.green: context.l10n.search_page_filter_green, + WarningLevel.yellow: context.l10n.search_page_filter_yellow, + WarningLevel.red: context.l10n.search_page_filter_red, + WarningLevel.none: context.l10n.search_page_filter_gray, + }[level]!, setState: (state) => cubit.search(showWarningLevel: {level: state}), state: filter?.showWarningLevel[level] ?? false)) ], From e29ac908fb2d6b81191e35a1db999ce6f1601b7a Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 15:59:02 +0100 Subject: [PATCH 30/35] feat(#479): improve checkmark row layout --- app/lib/common/widgets/context_menu.dart | 35 +++++++++++++----------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/app/lib/common/widgets/context_menu.dart b/app/lib/common/widgets/context_menu.dart index c84548db2..299d67b5e 100644 --- a/app/lib/common/widgets/context_menu.dart +++ b/app/lib/common/widgets/context_menu.dart @@ -25,20 +25,24 @@ class ContextMenu extends StatelessWidget { color: PharMeTheme.onSurfaceColor, borderRadius: BorderRadius.circular(8), ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: items - .mapIndexed((index, item) => (index == items.count() - 1) - ? item - : Container( - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - width: 0.5, color: PharMeTheme.borderColor), - )), - child: item)) - .toList(), + child: IntrinsicWidth( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: items + .mapIndexed( + (index, item) => (index == items.count() - 1) + ? item + : Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + width: 0.5, + color: PharMeTheme.borderColor), + )), + child: item)) + .toList(), + ), ), ), ), @@ -79,14 +83,13 @@ class ContextMenuCheckmark extends StatelessWidget { child: Padding( padding: EdgeInsets.all(12), child: Row( - mainAxisSize: MainAxisSize.min, children: [ if (state) Icon(Icons.check_rounded, size: 16) else SizedBox(width: 16, height: 16), SizedBox(width: 8), - Text(label), + Expanded(child: Text(label)), ], ), ), From 311fffaefc233b1ef95cb96611f09546bf51419a Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 16:00:55 +0100 Subject: [PATCH 31/35] chore(app): order deps --- app/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/pubspec.yaml b/app/pubspec.yaml index ac7aae1c5..3681435af 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -36,8 +36,8 @@ dependencies: intl: ^0.17.0 json_annotation: ^4.6.0 path_provider: ^2.0.11 - popover: ^0.2.8+1 pdf: ^3.8.1 + popover: ^0.2.8+1 shared_preferences: ^2.0.15 url_launcher: ^6.1.4 From f41ba33321f76abd738a94d17ffab679e5024049 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 16:01:16 +0100 Subject: [PATCH 32/35] test(#479): fix settings page test --- app/integration_test/settings_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/integration_test/settings_test.dart b/app/integration_test/settings_test.dart index 8a43fa50e..8954b95e7 100644 --- a/app/integration_test/settings_test.dart +++ b/app/integration_test/settings_test.dart @@ -44,7 +44,7 @@ void main() { ); // close dialog - await tester.tap(find.text(context.l10n.settings_page_cancel)); + await tester.tap(find.text(context.l10n.action_cancel)); await tester.pumpAndSettle(); // test onboarding button From 005f23b291af39975177d67b9e8e3bebfacf2343 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 16:16:04 +0100 Subject: [PATCH 33/35] fix(#479): linting --- app/lib/common/widgets/context_menu.dart | 7 ++++--- app/lib/search/pages/search.dart | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/lib/common/widgets/context_menu.dart b/app/lib/common/widgets/context_menu.dart index 299d67b5e..bb59f9565 100644 --- a/app/lib/common/widgets/context_menu.dart +++ b/app/lib/common/widgets/context_menu.dart @@ -60,18 +60,19 @@ class ContextMenu extends StatelessWidget { } class ContextMenuCheckmark extends StatelessWidget { - ContextMenuCheckmark( + const ContextMenuCheckmark( {super.key, required this.label, required this.setState, - this.state = false}); + this.initialState = false}); final String label; final void Function(bool) setState; - bool state; + final bool initialState; @override Widget build(BuildContext context) { + var state = initialState; return StatefulBuilder( builder: (context, rebuild) => GestureDetector( onTap: () { diff --git a/app/lib/search/pages/search.dart b/app/lib/search/pages/search.dart index 5798ebec8..b06e5284d 100644 --- a/app/lib/search/pages/search.dart +++ b/app/lib/search/pages/search.dart @@ -51,7 +51,7 @@ class SearchPage extends HookWidget { ContextMenuCheckmark( label: context.l10n.search_page_filter_inactive, setState: (state) => cubit.search(showInactive: state), - state: filter?.showInactive ?? false), + initialState: filter?.showInactive ?? false), ...WarningLevel.values.map((level) => ContextMenuCheckmark( label: { WarningLevel.green: context.l10n.search_page_filter_green, @@ -60,7 +60,7 @@ class SearchPage extends HookWidget { WarningLevel.none: context.l10n.search_page_filter_gray, }[level]!, setState: (state) => cubit.search(showWarningLevel: {level: state}), - state: filter?.showWarningLevel[level] ?? false)) + initialState: filter?.showWarningLevel[level] ?? false)) ], child: Padding( padding: EdgeInsets.all(8), child: Icon(Icons.filter_list_rounded)), From c0b9fac79ba69211b03c279e6ec7379cc66e6660 Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Wed, 22 Feb 2023 16:26:11 +0100 Subject: [PATCH 34/35] test(#479): fix search test --- app/integration_test/search_test.dart | 4 ++-- app/lib/search/pages/cubit.dart | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/integration_test/search_test.dart b/app/integration_test/search_test.dart index 22884a99c..9f0cffc71 100644 --- a/app/integration_test/search_test.dart +++ b/app/integration_test/search_test.dart @@ -8,7 +8,7 @@ import 'package:mocktail/mocktail.dart'; class MockSearchCubit extends MockCubit implements SearchCubit { @override - bool get filterActive => false; + FilterState get filter => FilterState.initial(); } void main() { @@ -71,7 +71,7 @@ void main() { testWidgets('test search page in loaded state', (tester) async { when(() => mockSearchCubit.state) - .thenReturn(SearchState.loaded(loadedDrugs, loadedDrugs)); + .thenReturn(SearchState.loaded(loadedDrugs, FilterState.initial())); await tester.pumpWidget(BlocProvider.value( value: mockSearchCubit, diff --git a/app/lib/search/pages/cubit.dart b/app/lib/search/pages/cubit.dart index 58b73c8d2..2a0de9c82 100644 --- a/app/lib/search/pages/cubit.dart +++ b/app/lib/search/pages/cubit.dart @@ -95,7 +95,7 @@ class FilterState { final warningLevel = drug.userGuideline()?.annotations.warningLevel; return drug.matches(query: query) && (drug.isActive() || showInactive) && - showWarningLevel[warningLevel]!; + (showWarningLevel[warningLevel] ?? true); } List filter(List drugs) => drugs.filter(isAccepted).toList(); From 5148954280b856b4d0ce322855df723f3a9463ac Mon Sep 17 00:00:00 2001 From: Jannis Baum Date: Sat, 25 Feb 2023 09:25:38 -0500 Subject: [PATCH 35/35] ci: upgrade flutter action --- .github/workflows/app.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/app.yml b/.github/workflows/app.yml index c8466b860..1d55943d6 100644 --- a/.github/workflows/app.yml +++ b/.github/workflows/app.yml @@ -13,7 +13,7 @@ defaults: env: JAVA_VERSION: 12.x FLUTTER_CHANNEL: stable - FLUTTER_VERSION: 3.0.3 + FLUTTER_VERSION: 3.3.4 jobs: lint: @@ -24,7 +24,7 @@ jobs: - uses: actions/setup-java@v1 with: java-version: ${{ env.JAVA_VERSION }} - - uses: subosito/flutter-action@v1 + - uses: subosito/flutter-action@v2 with: channel: ${{ env.FLUTTER_CHANNEL }} flutter-version: ${{ env.FLUTTER_VERSION }}