From 4957068d6cb6944d0847b8faa518b1522b4314a3 Mon Sep 17 00:00:00 2001 From: Carlos Cruz Date: Tue, 28 Jul 2026 16:23:10 +0100 Subject: [PATCH 1/9] feat: tag widget definitions with entity-relationship categories Adds a category field (Target, Disease, Drug, Target-Disease, Target-Variant, Disease-Variant, Literature) to every widget's definition object across all 71 sections, reusing the same taxonomy already used for dataset tagging on the Downloads page. Groundwork for category-driven icons and filtering across profile pages. --- .../EnhancerToGenePredictions/index.ts | 1 + .../src/credibleSet/GWASColoc/index.ts | 1 + .../src/credibleSet/Locus2Gene/index.ts | 1 + .../src/credibleSet/MolQTLColoc/index.ts | 1 + .../src/credibleSet/Variants/index.ts | 1 + .../src/disease/Bibliography/index.ts | 1 + packages/sections/src/disease/Drugs/index.ts | 1 + .../sections/src/disease/GWASStudies/index.ts | 1 + .../sections/src/disease/OTProjects/index.ts | 1 + .../sections/src/disease/Ontology/index.ts | 1 + .../sections/src/disease/Phenotypes/index.ts | 1 + .../sections/src/drug/AdverseEvents/index.ts | 1 + .../sections/src/drug/Bibliography/index.ts | 1 + .../sections/src/drug/DrugWarnings/index.ts | 1 + .../sections/src/drug/Indications/index.ts | 1 + .../src/drug/MechanismsOfAction/index.ts | 1 + .../src/drug/Pharmacogenomics/index.ts | 1 + .../sections/src/evidence/CRISPR/index.ts | 1 + .../src/evidence/CRISPRScreen/index.ts | 1 + .../src/evidence/CancerBiomarkers/index.ts | 1 + .../src/evidence/CancerGeneCensus/index.ts | 1 + .../sections/src/evidence/ClinGen/index.ts | 1 + .../src/evidence/ClinicalPrecedence/index.tsx | 1 + packages/sections/src/evidence/EVA/index.ts | 1 + .../sections/src/evidence/EVASomatic/index.ts | 1 + .../sections/src/evidence/EuropePmc/index.ts | 1 + .../src/evidence/ExpressionAtlas/index.ts | 1 + .../src/evidence/GWASCredibleSets/index.ts | 1 + .../src/evidence/Gene2Phenotype/index.ts | 1 + .../sections/src/evidence/GeneBurden/index.ts | 1 + .../src/evidence/GenomicsEngland/index.ts | 1 + packages/sections/src/evidence/Impc/index.ts | 1 + .../sections/src/evidence/IntOgen/index.ts | 1 + .../sections/src/evidence/OTCRISPR/index.ts | 1 + .../sections/src/evidence/OTEncore/index.ts | 1 + .../src/evidence/OTValidation/index.ts | 1 + .../sections/src/evidence/Orphanet/index.ts | 1 + .../sections/src/evidence/Reactome/index.ts | 1 + .../src/evidence/UniProtLiterature/index.ts | 1 + .../src/evidence/UniProtVariants/index.ts | 1 + .../src/study/GWASCredibleSets/index.ts | 1 + .../src/study/QTLCredibleSets/index.ts | 1 + .../src/study/SharedTraitStudies/index.ts | 1 + .../src/target/BaselineExpression/index.ts | 1 + .../sections/src/target/Bibliography/index.ts | 1 + .../src/target/CancerHallmarks/index.ts | 1 + .../src/target/ChemicalProbes/index.ts | 1 + .../src/target/ComparativeGenomics/index.ts | 1 + packages/sections/src/target/DepMap/index.ts | 1 + packages/sections/src/target/Drugs/index.ts | 1 + .../sections/src/target/GeneOntology/index.ts | 1 + .../src/target/GeneticConstraint/index.ts | 1 + .../src/target/MolecularInteractions/index.ts | 1 + .../src/target/MolecularStructure/index.ts | 1 + .../src/target/MousePhenotypes/index.ts | 1 + .../src/target/OverlappingVariants/index.ts | 1 + .../sections/src/target/Pathways/index.ts | 1 + .../src/target/Pharmacogenomics/index.ts | 1 + .../src/target/QTLCredibleSets/index.ts | 1 + packages/sections/src/target/Safety/index.ts | 1 + .../src/target/SubcellularLocation/index.ts | 1 + .../sections/src/target/Tractability/index.ts | 1 + packages/sections/src/types/widget.ts | 3 ++ packages/sections/src/variant/EVA/index.ts | 1 + .../EnhancerToGenePredictions/index.ts | 1 + .../src/variant/GWASCredibleSets/index.ts | 1 + .../src/variant/MolecularStructure/index.ts | 1 + .../src/variant/Pharmacogenomics/index.ts | 1 + .../src/variant/QTLCredibleSets/index.ts | 1 + .../src/variant/UniProtVariants/index.ts | 1 + .../src/variant/VariantEffect/index.ts | 1 + .../variant/VariantEffectPredictor/index.ts | 1 + .../src/components/Summary/categoryConfig.ts | 32 +++++++++++++++++++ 73 files changed, 106 insertions(+) create mode 100644 packages/ui/src/components/Summary/categoryConfig.ts diff --git a/packages/sections/src/credibleSet/EnhancerToGenePredictions/index.ts b/packages/sections/src/credibleSet/EnhancerToGenePredictions/index.ts index a6a6f4358..e24c51195 100644 --- a/packages/sections/src/credibleSet/EnhancerToGenePredictions/index.ts +++ b/packages/sections/src/credibleSet/EnhancerToGenePredictions/index.ts @@ -5,6 +5,7 @@ export const definition = { id: "Enhancer_to_gene_predictions", name: "Enhancer-to-gene predictions", shortName: "EG", + category: "Target-Variant", hasData: (data: CredibleSet) => { return (data.variant?.enhancerToGenes?.count || 0) > 0; }, diff --git a/packages/sections/src/credibleSet/GWASColoc/index.ts b/packages/sections/src/credibleSet/GWASColoc/index.ts index c43b6f131..9cad8300d 100644 --- a/packages/sections/src/credibleSet/GWASColoc/index.ts +++ b/packages/sections/src/credibleSet/GWASColoc/index.ts @@ -5,6 +5,7 @@ export const definition = { id: "gwas_coloc", name: "GWAS Colocalisation", shortName: "GC", + category: "Disease-Variant", hasData: (data: CredibleSet) => (data.colocalisation?.count || 0) > 0, }; diff --git a/packages/sections/src/credibleSet/Locus2Gene/index.ts b/packages/sections/src/credibleSet/Locus2Gene/index.ts index c59167cfe..29b22b79c 100644 --- a/packages/sections/src/credibleSet/Locus2Gene/index.ts +++ b/packages/sections/src/credibleSet/Locus2Gene/index.ts @@ -5,6 +5,7 @@ export const definition = { id: "locus2gene", name: "Locus-to-Gene", shortName: "LG", + category: "Target-Variant", hasData: (data: CredibleSet) => (data.l2GPredictions?.count || 0) > 0, }; diff --git a/packages/sections/src/credibleSet/MolQTLColoc/index.ts b/packages/sections/src/credibleSet/MolQTLColoc/index.ts index f9b546605..e613b44f4 100644 --- a/packages/sections/src/credibleSet/MolQTLColoc/index.ts +++ b/packages/sections/src/credibleSet/MolQTLColoc/index.ts @@ -5,6 +5,7 @@ export const definition = { id: "molqtl_coloc", name: "MolQTL Colocalisation", shortName: "QC", + category: "Target-Variant", hasData: (data: CredibleSet) => (data.molqtlcolocalisation?.count || 0) > 0, }; diff --git a/packages/sections/src/credibleSet/Variants/index.ts b/packages/sections/src/credibleSet/Variants/index.ts index 8fb8b181a..710d1b69b 100644 --- a/packages/sections/src/credibleSet/Variants/index.ts +++ b/packages/sections/src/credibleSet/Variants/index.ts @@ -5,6 +5,7 @@ export const definition = { id: "variants", name: "Credible Set Variants", shortName: "VA", + category: ["Target-Variant", "Disease-Variant"], hasData: (data: CredibleSet) => (data.locus?.count || 0) > 0, }; diff --git a/packages/sections/src/disease/Bibliography/index.ts b/packages/sections/src/disease/Bibliography/index.ts index fbdfd8ae5..257a049f0 100644 --- a/packages/sections/src/disease/Bibliography/index.ts +++ b/packages/sections/src/disease/Bibliography/index.ts @@ -4,6 +4,7 @@ export const definition = { id: "bibliography", name: "Bibliography", shortName: "B", + category: "Literature", hasData: (data: any) => data.literatureOcurrences?.filteredCount > 0, }; diff --git a/packages/sections/src/disease/Drugs/index.ts b/packages/sections/src/disease/Drugs/index.ts index cfbad3d7b..349ff4ed7 100644 --- a/packages/sections/src/disease/Drugs/index.ts +++ b/packages/sections/src/disease/Drugs/index.ts @@ -4,6 +4,7 @@ export const definition = { id: "drugs", name: "Drugs and Clinical Candidates", shortName: "DC", + category: ["Target-Disease", "Drug"], hasData: (data) => data?.drugAndClinicalCandidates?.count > 0, }; diff --git a/packages/sections/src/disease/GWASStudies/index.ts b/packages/sections/src/disease/GWASStudies/index.ts index 4b68d74eb..5bbc90686 100644 --- a/packages/sections/src/disease/GWASStudies/index.ts +++ b/packages/sections/src/disease/GWASStudies/index.ts @@ -4,6 +4,7 @@ export const definition = { id: "GWASStudies", name: "GWAS", shortName: "GS", + category: "Disease-Variant", hasData: data => data?.studies?.count > 0 || // summary data?.count > 0, // section diff --git a/packages/sections/src/disease/OTProjects/index.ts b/packages/sections/src/disease/OTProjects/index.ts index bd84c59fe..5b8c4f838 100644 --- a/packages/sections/src/disease/OTProjects/index.ts +++ b/packages/sections/src/disease/OTProjects/index.ts @@ -7,6 +7,7 @@ export const definition = { id, name: "Open Targets Projects", shortName: "OP", + category: "Disease", hasData: (data: any) => data.otarProjects?.length > 0, isPrivate: isPrivateDiseaseSection(id), }; diff --git a/packages/sections/src/disease/Ontology/index.ts b/packages/sections/src/disease/Ontology/index.ts index 445d69f92..2350e9b9c 100644 --- a/packages/sections/src/disease/Ontology/index.ts +++ b/packages/sections/src/disease/Ontology/index.ts @@ -4,6 +4,7 @@ export const definition = { id: "ontology", name: "Ontology", shortName: "O", + category: "Disease", hasData: (data: any) => data.id, }; diff --git a/packages/sections/src/disease/Phenotypes/index.ts b/packages/sections/src/disease/Phenotypes/index.ts index 477ff8a1f..1d65672b7 100644 --- a/packages/sections/src/disease/Phenotypes/index.ts +++ b/packages/sections/src/disease/Phenotypes/index.ts @@ -4,6 +4,7 @@ export const definition = { id: "phenotypes", name: "Clinical signs and symptoms", shortName: "CS", + category: "Disease", hasData: (data: any) => (data.phenotypes?.count || 0) > 0, }; diff --git a/packages/sections/src/drug/AdverseEvents/index.ts b/packages/sections/src/drug/AdverseEvents/index.ts index bb01b5a45..1cfbfb418 100644 --- a/packages/sections/src/drug/AdverseEvents/index.ts +++ b/packages/sections/src/drug/AdverseEvents/index.ts @@ -4,6 +4,7 @@ export const definition = { id: "adverseEvents", name: "Pharmacovigilance", shortName: "PV", + category: "Drug", hasData: (data: any) => data.adverseEvents?.count > 0 || false, }; diff --git a/packages/sections/src/drug/Bibliography/index.ts b/packages/sections/src/drug/Bibliography/index.ts index b18901439..7ed261119 100644 --- a/packages/sections/src/drug/Bibliography/index.ts +++ b/packages/sections/src/drug/Bibliography/index.ts @@ -4,6 +4,7 @@ export const definition = { id: "bibliography", name: "Bibliography", shortName: "B", + category: "Literature", hasData: (data: any) => data.literatureOcurrences?.filteredCount > 0, }; diff --git a/packages/sections/src/drug/DrugWarnings/index.ts b/packages/sections/src/drug/DrugWarnings/index.ts index ac6e8f909..63c4d0f42 100644 --- a/packages/sections/src/drug/DrugWarnings/index.ts +++ b/packages/sections/src/drug/DrugWarnings/index.ts @@ -4,6 +4,7 @@ export const definition = { id: "drugWarnings", name: "Drug Warnings", shortName: "DW", + category: "Drug", hasData: ({ drugWarnings }: { drugWarnings?: { warningType: string }[] }) => (drugWarnings?.length ?? 0) > 0, }; diff --git a/packages/sections/src/drug/Indications/index.ts b/packages/sections/src/drug/Indications/index.ts index d9c763051..e56910e1f 100644 --- a/packages/sections/src/drug/Indications/index.ts +++ b/packages/sections/src/drug/Indications/index.ts @@ -4,6 +4,7 @@ export const definition = { id: "Indications", name: "Indications", shortName: "I", + category: ["Target-Disease", "Drug"], hasData: (data) => data?.indications?.count > 0, }; diff --git a/packages/sections/src/drug/MechanismsOfAction/index.ts b/packages/sections/src/drug/MechanismsOfAction/index.ts index c9d5de035..4d00842b1 100644 --- a/packages/sections/src/drug/MechanismsOfAction/index.ts +++ b/packages/sections/src/drug/MechanismsOfAction/index.ts @@ -4,6 +4,7 @@ export const definition = { id: "mechanismsOfAction", name: "Mechanisms of Action", shortName: "MA", + category: "Drug", hasData: (data: any) => (data.mechanismsOfAction?.uniqueActionTypes.length > 0 && data.mechanismsOfAction?.uniqueTargetTypes.length > 0) || diff --git a/packages/sections/src/drug/Pharmacogenomics/index.ts b/packages/sections/src/drug/Pharmacogenomics/index.ts index 424699e29..6e6ab474d 100644 --- a/packages/sections/src/drug/Pharmacogenomics/index.ts +++ b/packages/sections/src/drug/Pharmacogenomics/index.ts @@ -4,6 +4,7 @@ export const definition = { id: "pharmacogenetics", name: "Pharmacogenetics", shortName: "PGx", + category: "Drug", hasData: (data: any) => data.pharmacogenomics.length > 0, }; diff --git a/packages/sections/src/evidence/CRISPR/index.ts b/packages/sections/src/evidence/CRISPR/index.ts index 776262bd3..80a38281d 100644 --- a/packages/sections/src/evidence/CRISPR/index.ts +++ b/packages/sections/src/evidence/CRISPR/index.ts @@ -7,6 +7,7 @@ export const definition = { id, name: "Project Score", shortName: "PS", + category: "Target-Disease", hasData: (data: EvidenceData) => (data.crisprSummary?.count || 0) > 0, isPrivate: isPrivateEvidenceSection(id), }; diff --git a/packages/sections/src/evidence/CRISPRScreen/index.ts b/packages/sections/src/evidence/CRISPRScreen/index.ts index e71e77bf6..cb273c69c 100644 --- a/packages/sections/src/evidence/CRISPRScreen/index.ts +++ b/packages/sections/src/evidence/CRISPRScreen/index.ts @@ -7,6 +7,7 @@ export const definition = { id, name: "CRISPR Screens", shortName: "CS", + category: "Target-Disease", hasData: (data: EvidenceData) => (data.CrisprScreenSummary?.count || 0) > 0, isPrivate: isPrivateEvidenceSection(id), }; diff --git a/packages/sections/src/evidence/CancerBiomarkers/index.ts b/packages/sections/src/evidence/CancerBiomarkers/index.ts index a142d7356..840ea4b0c 100644 --- a/packages/sections/src/evidence/CancerBiomarkers/index.ts +++ b/packages/sections/src/evidence/CancerBiomarkers/index.ts @@ -7,6 +7,7 @@ export const definition = { id, name: "Cancer Biomarkers", shortName: "CB", + category: ["Target-Disease", "Drug"], hasData: (data: EvidenceData) => (data.cancerBiomarkersSummary?.count || 0) > 0, isPrivate: isPrivateEvidenceSection(id), dataType: dataTypesMap.affected_pathway, diff --git a/packages/sections/src/evidence/CancerGeneCensus/index.ts b/packages/sections/src/evidence/CancerGeneCensus/index.ts index 204536c02..0c7ec93bb 100644 --- a/packages/sections/src/evidence/CancerGeneCensus/index.ts +++ b/packages/sections/src/evidence/CancerGeneCensus/index.ts @@ -7,6 +7,7 @@ export const definition = { id, name: "Cancer Gene Census", shortName: "CC", + category: "Target-Disease", hasData: (data: EvidenceData) => (data.cancerGeneCensusSummary?.count || 0) > 0, isPrivate: isPrivateEvidenceSection(id), }; diff --git a/packages/sections/src/evidence/ClinGen/index.ts b/packages/sections/src/evidence/ClinGen/index.ts index b6136394c..dfb2ac733 100644 --- a/packages/sections/src/evidence/ClinGen/index.ts +++ b/packages/sections/src/evidence/ClinGen/index.ts @@ -7,6 +7,7 @@ export const definition = { id, name: "ClinGen", shortName: "CG", + category: "Target-Disease", hasData: (data: EvidenceData) => (data.clingenSummary?.count || 0) > 0, isPrivate: isPrivateEvidenceSection(id), }; diff --git a/packages/sections/src/evidence/ClinicalPrecedence/index.tsx b/packages/sections/src/evidence/ClinicalPrecedence/index.tsx index 4d1607106..94474df39 100644 --- a/packages/sections/src/evidence/ClinicalPrecedence/index.tsx +++ b/packages/sections/src/evidence/ClinicalPrecedence/index.tsx @@ -7,6 +7,7 @@ export const definition = { id, name: "Clinical Precedence", shortName: "CP", + category: ["Target-Disease", "Drug"], hasData: (data: EvidenceData) => (data.clinical_precedence?.count || 0) > 0, isPrivate: isPrivateEvidenceSection(id), }; diff --git a/packages/sections/src/evidence/EVA/index.ts b/packages/sections/src/evidence/EVA/index.ts index bdc83d95c..c15723bac 100644 --- a/packages/sections/src/evidence/EVA/index.ts +++ b/packages/sections/src/evidence/EVA/index.ts @@ -7,6 +7,7 @@ export const definition = { id, name: "ClinVar", shortName: "CV", + category: ["Target-Disease", "Disease-Variant"], hasData: (data: EvidenceData) => (data.eva?.count || 0) > 0, isPrivate: isPrivateEvidenceSection(id), }; diff --git a/packages/sections/src/evidence/EVASomatic/index.ts b/packages/sections/src/evidence/EVASomatic/index.ts index 4901c6dcd..a97439950 100644 --- a/packages/sections/src/evidence/EVASomatic/index.ts +++ b/packages/sections/src/evidence/EVASomatic/index.ts @@ -7,6 +7,7 @@ export const definition = { id, name: "EVA somatic", shortName: "ES", + category: ["Target-Disease", "Disease-Variant"], hasData: (data: EvidenceData) => (data.eva_somatic?.count || 0) > 0, isPrivate: isPrivateEvidenceSection(id), }; diff --git a/packages/sections/src/evidence/EuropePmc/index.ts b/packages/sections/src/evidence/EuropePmc/index.ts index e53828245..53f62203e 100644 --- a/packages/sections/src/evidence/EuropePmc/index.ts +++ b/packages/sections/src/evidence/EuropePmc/index.ts @@ -7,6 +7,7 @@ export const definition = { id, name: "Europe PMC", shortName: "EP", + category: "Literature", hasData: (data: EvidenceData) => (data.europePmc?.count || 0) > 0, isPrivate: isPrivateEvidenceSection(id), }; diff --git a/packages/sections/src/evidence/ExpressionAtlas/index.ts b/packages/sections/src/evidence/ExpressionAtlas/index.ts index 4f6ddeb96..5bad6cb69 100644 --- a/packages/sections/src/evidence/ExpressionAtlas/index.ts +++ b/packages/sections/src/evidence/ExpressionAtlas/index.ts @@ -7,6 +7,7 @@ export const definition = { id, name: "Expression Atlas", shortName: "EA", + category: "Target-Disease", hasData: (data: EvidenceData) => (data.expressionAtlasSummary?.count || 0) > 0, isPrivate: isPrivateEvidenceSection(id), }; diff --git a/packages/sections/src/evidence/GWASCredibleSets/index.ts b/packages/sections/src/evidence/GWASCredibleSets/index.ts index 07258a638..bc5cb3054 100644 --- a/packages/sections/src/evidence/GWASCredibleSets/index.ts +++ b/packages/sections/src/evidence/GWASCredibleSets/index.ts @@ -7,6 +7,7 @@ export const definition = { id, name: "GWAS associations", shortName: "GW", + category: "Disease-Variant", hasData: (data: EvidenceData) => (data.gwasCredibleSets?.count || 0) > 0, isPrivate: isPrivateEvidenceSection(id), }; diff --git a/packages/sections/src/evidence/Gene2Phenotype/index.ts b/packages/sections/src/evidence/Gene2Phenotype/index.ts index 7d1215ebb..dd122bce6 100644 --- a/packages/sections/src/evidence/Gene2Phenotype/index.ts +++ b/packages/sections/src/evidence/Gene2Phenotype/index.ts @@ -7,6 +7,7 @@ export const definition = { id, name: "Gene2Phenotype", shortName: "GP", + category: "Target-Disease", hasData: (data: EvidenceData) => (data.gene2Phenotype?.count || 0) > 0, isPrivate: isPrivateEvidenceSection(id), }; diff --git a/packages/sections/src/evidence/GeneBurden/index.ts b/packages/sections/src/evidence/GeneBurden/index.ts index 9bfc14db8..c9bb1d819 100644 --- a/packages/sections/src/evidence/GeneBurden/index.ts +++ b/packages/sections/src/evidence/GeneBurden/index.ts @@ -7,6 +7,7 @@ export const definition = { id, name: "Gene Burden", shortName: "GB", + category: ["Target-Disease", "Disease-Variant"], hasData: (data: EvidenceData) => (data.geneBurdenSummary?.count || 0) > 0, isPrivate: isPrivateEvidenceSection(id), }; diff --git a/packages/sections/src/evidence/GenomicsEngland/index.ts b/packages/sections/src/evidence/GenomicsEngland/index.ts index cfa7ee1f2..1a63bc635 100644 --- a/packages/sections/src/evidence/GenomicsEngland/index.ts +++ b/packages/sections/src/evidence/GenomicsEngland/index.ts @@ -7,6 +7,7 @@ export const definition = { id, name: "GEL PanelApp", shortName: "GE", + category: "Target-Disease", hasData: (data: EvidenceData) => (data.genomicsEngland?.count || 0) > 0, isPrivate: isPrivateEvidenceSection(id), }; diff --git a/packages/sections/src/evidence/Impc/index.ts b/packages/sections/src/evidence/Impc/index.ts index d050881c6..7f04b0533 100644 --- a/packages/sections/src/evidence/Impc/index.ts +++ b/packages/sections/src/evidence/Impc/index.ts @@ -7,6 +7,7 @@ export const definition = { id, name: "IMPC", shortName: "IM", + category: "Target-Disease", hasData: (data: EvidenceData) => (data.impc?.count || 0) > 0, isPrivate: isPrivateEvidenceSection(id), }; diff --git a/packages/sections/src/evidence/IntOgen/index.ts b/packages/sections/src/evidence/IntOgen/index.ts index e03fe74be..978221978 100644 --- a/packages/sections/src/evidence/IntOgen/index.ts +++ b/packages/sections/src/evidence/IntOgen/index.ts @@ -7,6 +7,7 @@ export const definition = { id, name: "IntOGen", shortName: "IO", + category: "Target-Disease", hasData: (data: EvidenceData) => (data.intOgen?.count || 0) > 0, isPrivate: isPrivateEvidenceSection(id), }; diff --git a/packages/sections/src/evidence/OTCRISPR/index.ts b/packages/sections/src/evidence/OTCRISPR/index.ts index 20ce26325..03794bf79 100644 --- a/packages/sections/src/evidence/OTCRISPR/index.ts +++ b/packages/sections/src/evidence/OTCRISPR/index.ts @@ -7,6 +7,7 @@ export const definition = { id, name: "Open Targets CRISPR", shortName: "OT", + category: "Target-Disease", hasData: (data: EvidenceData) => (data.OtCrisprSummary?.count || 0) > 0, isPrivate: isPrivateEvidenceSection(id), }; diff --git a/packages/sections/src/evidence/OTEncore/index.ts b/packages/sections/src/evidence/OTEncore/index.ts index 4a0cc98ed..cc0523dc5 100644 --- a/packages/sections/src/evidence/OTEncore/index.ts +++ b/packages/sections/src/evidence/OTEncore/index.ts @@ -7,6 +7,7 @@ export const definition = { id, name: "Open Targets ENCORE", shortName: "OT", + category: "Target-Disease", hasData: (data: EvidenceData) => (data.otEncoreSummary?.count || 0) > 0, isPrivate: isPrivateEvidenceSection(id), }; diff --git a/packages/sections/src/evidence/OTValidation/index.ts b/packages/sections/src/evidence/OTValidation/index.ts index bae27c5fd..75f590b2c 100644 --- a/packages/sections/src/evidence/OTValidation/index.ts +++ b/packages/sections/src/evidence/OTValidation/index.ts @@ -7,6 +7,7 @@ export const definition = { id, name: "Open Targets Validation CRISPR", shortName: "VL", + category: "Target-Disease", hasData: (data: EvidenceData) => (data.otValidationSummary?.count || 0) > 0, isPrivate: isPrivateEvidenceSection(id), }; diff --git a/packages/sections/src/evidence/Orphanet/index.ts b/packages/sections/src/evidence/Orphanet/index.ts index 7dc43b83f..c3a9462ac 100644 --- a/packages/sections/src/evidence/Orphanet/index.ts +++ b/packages/sections/src/evidence/Orphanet/index.ts @@ -7,6 +7,7 @@ export const definition = { id, name: "Orphanet", shortName: "ON", + category: "Target-Disease", hasData: (data: EvidenceData) => (data.orphanetSummary?.count || 0) > 0, isPrivate: isPrivateEvidenceSection(id), }; diff --git a/packages/sections/src/evidence/Reactome/index.ts b/packages/sections/src/evidence/Reactome/index.ts index 8422479b2..7433fe52e 100644 --- a/packages/sections/src/evidence/Reactome/index.ts +++ b/packages/sections/src/evidence/Reactome/index.ts @@ -7,6 +7,7 @@ export const definition = { id, name: "Reactome", shortName: "RE", + category: "Target-Disease", hasData: (data: EvidenceData) => (data.reactomeSummary?.count || 0) > 0, isPrivate: isPrivateEvidenceSection(id), }; diff --git a/packages/sections/src/evidence/UniProtLiterature/index.ts b/packages/sections/src/evidence/UniProtLiterature/index.ts index 3c813b203..cbf02fe26 100644 --- a/packages/sections/src/evidence/UniProtLiterature/index.ts +++ b/packages/sections/src/evidence/UniProtLiterature/index.ts @@ -7,6 +7,7 @@ export const definition = { id, name: "UniProt literature", shortName: "UL", + category: ["Target-Disease", "Literature"], hasData: (data: EvidenceData) => (data.uniprotLiteratureSummary?.count || 0) > 0, isPrivate: isPrivateEvidenceSection(id), }; diff --git a/packages/sections/src/evidence/UniProtVariants/index.ts b/packages/sections/src/evidence/UniProtVariants/index.ts index 04a982974..49749ca29 100644 --- a/packages/sections/src/evidence/UniProtVariants/index.ts +++ b/packages/sections/src/evidence/UniProtVariants/index.ts @@ -7,6 +7,7 @@ export const definition = { id, name: "UniProt variants", shortName: "UV", + category: "Disease-Variant", hasData: (data: EvidenceData) => (data.uniprotVariantsSummary?.count || 0) > 0, isPrivate: isPrivateEvidenceSection(id), }; diff --git a/packages/sections/src/study/GWASCredibleSets/index.ts b/packages/sections/src/study/GWASCredibleSets/index.ts index 2ade8cda6..b02168e21 100644 --- a/packages/sections/src/study/GWASCredibleSets/index.ts +++ b/packages/sections/src/study/GWASCredibleSets/index.ts @@ -5,6 +5,7 @@ export const definition = { id: "gwas_credible_sets", name: "GWAS Credible Sets", shortName: "GW", + category: "Disease-Variant", hasData: (data: Study) => (data.credibleSets?.count || 0) > 0, }; diff --git a/packages/sections/src/study/QTLCredibleSets/index.ts b/packages/sections/src/study/QTLCredibleSets/index.ts index fac088e64..156b866cb 100644 --- a/packages/sections/src/study/QTLCredibleSets/index.ts +++ b/packages/sections/src/study/QTLCredibleSets/index.ts @@ -5,6 +5,7 @@ export const definition = { id: "qtl_credible_sets", name: "molQTL Credible Sets", shortName: "QT", + category: "Target-Variant", // @ts-expect-error TODO: fix this hasData: (data: Study) => data?.qtlCredibleSets?.count > 0 || data?.credibleSets?.count > 0, }; diff --git a/packages/sections/src/study/SharedTraitStudies/index.ts b/packages/sections/src/study/SharedTraitStudies/index.ts index 538581859..674f004aa 100644 --- a/packages/sections/src/study/SharedTraitStudies/index.ts +++ b/packages/sections/src/study/SharedTraitStudies/index.ts @@ -5,6 +5,7 @@ export const definition = { id: "shared_trait_studies", name: "Shared Trait Studies", shortName: "ST", + category: "Disease-Variant", hasData: (data: Study) => { // @ts-expect-error TODO: check this return data?.sharedTraitStudies?.count > 1 || data?.count > 1; diff --git a/packages/sections/src/target/BaselineExpression/index.ts b/packages/sections/src/target/BaselineExpression/index.ts index bcef33109..1b4a9c156 100644 --- a/packages/sections/src/target/BaselineExpression/index.ts +++ b/packages/sections/src/target/BaselineExpression/index.ts @@ -5,6 +5,7 @@ export const definition = { id: "baselineExpression", name: "Baseline Expression", shortName: "BE", + category: "Target", hasData: (data: TargetData) => { return ( (data.baselineExpression?.count || 0) > 0 || // main widget tab diff --git a/packages/sections/src/target/Bibliography/index.ts b/packages/sections/src/target/Bibliography/index.ts index 4b81d6a21..9c397bb24 100644 --- a/packages/sections/src/target/Bibliography/index.ts +++ b/packages/sections/src/target/Bibliography/index.ts @@ -5,6 +5,7 @@ export const definition = { id: "bibliography", name: "Bibliography", shortName: "B", + category: "Literature", hasData: (data: TargetData) => (data.literatureOcurrences?.filteredCount || 0) > 0, }; diff --git a/packages/sections/src/target/CancerHallmarks/index.ts b/packages/sections/src/target/CancerHallmarks/index.ts index 6c2ee7c41..841442e61 100644 --- a/packages/sections/src/target/CancerHallmarks/index.ts +++ b/packages/sections/src/target/CancerHallmarks/index.ts @@ -5,6 +5,7 @@ export const definition = { id: "cancerHallmarks", name: "Cancer Hallmarks", shortName: "CH", + category: "Target-Disease", hasData: (data: TargetData) => (data.hallmarks?.cancerHallmarks?.length || 0) > 0, }; diff --git a/packages/sections/src/target/ChemicalProbes/index.ts b/packages/sections/src/target/ChemicalProbes/index.ts index 3bccb1edd..9bf807300 100644 --- a/packages/sections/src/target/ChemicalProbes/index.ts +++ b/packages/sections/src/target/ChemicalProbes/index.ts @@ -5,6 +5,7 @@ export const definition = { id: "chemicalProbes", name: "Chemical Probes", shortName: "CP", + category: "Target", hasData: (data: TargetData) => (data.chemicalProbes?.length || 0) > 0, }; diff --git a/packages/sections/src/target/ComparativeGenomics/index.ts b/packages/sections/src/target/ComparativeGenomics/index.ts index ac252c0b0..d9b277220 100644 --- a/packages/sections/src/target/ComparativeGenomics/index.ts +++ b/packages/sections/src/target/ComparativeGenomics/index.ts @@ -6,6 +6,7 @@ export const definition = { id: "compGenomics", name: "Comparative Genomics", shortName: "CG", + category: "Target", hasData: (data: TargetData) => { const { paralogueCount, orthologueCount } = countHomologues(data.homologues || []); return paralogueCount > 0 || orthologueCount > 0; diff --git a/packages/sections/src/target/DepMap/index.ts b/packages/sections/src/target/DepMap/index.ts index d837cff6b..f0e8a5523 100644 --- a/packages/sections/src/target/DepMap/index.ts +++ b/packages/sections/src/target/DepMap/index.ts @@ -5,6 +5,7 @@ export const definition = { id: "depMapEssentiality", name: "Cancer DepMap", shortName: "DM", + category: "Target-Disease", hasData: (data: TargetData) => (data.depMapEssentiality?.length || 0) > 0, }; diff --git a/packages/sections/src/target/Drugs/index.ts b/packages/sections/src/target/Drugs/index.ts index cfbad3d7b..349ff4ed7 100644 --- a/packages/sections/src/target/Drugs/index.ts +++ b/packages/sections/src/target/Drugs/index.ts @@ -4,6 +4,7 @@ export const definition = { id: "drugs", name: "Drugs and Clinical Candidates", shortName: "DC", + category: ["Target-Disease", "Drug"], hasData: (data) => data?.drugAndClinicalCandidates?.count > 0, }; diff --git a/packages/sections/src/target/GeneOntology/index.ts b/packages/sections/src/target/GeneOntology/index.ts index 78f27606d..56301a9a6 100644 --- a/packages/sections/src/target/GeneOntology/index.ts +++ b/packages/sections/src/target/GeneOntology/index.ts @@ -5,6 +5,7 @@ export const definition = { id: "geneOntology", name: "Gene Ontology", shortName: "GO", + category: "Target", hasData: (data: TargetData) => (data.geneOntology?.length || 0) > 0, }; diff --git a/packages/sections/src/target/GeneticConstraint/index.ts b/packages/sections/src/target/GeneticConstraint/index.ts index 43dddfaf4..6686fc1d6 100644 --- a/packages/sections/src/target/GeneticConstraint/index.ts +++ b/packages/sections/src/target/GeneticConstraint/index.ts @@ -5,6 +5,7 @@ export const definition = { id: "geneticConstraint", name: "Genetic Constraint", shortName: "GC", + category: "Target-Variant", hasData: (data: TargetData) => (data.geneticConstraint?.length || 0) > 0, }; diff --git a/packages/sections/src/target/MolecularInteractions/index.ts b/packages/sections/src/target/MolecularInteractions/index.ts index 7a70145c4..7cfcbbe37 100644 --- a/packages/sections/src/target/MolecularInteractions/index.ts +++ b/packages/sections/src/target/MolecularInteractions/index.ts @@ -5,6 +5,7 @@ export const definition = { id: "interactions", name: "Molecular Interactions", shortName: "MI", + category: "Target", hasData: (data: TargetData) => (data.interactions?.count || 0) > 0 || false, }; diff --git a/packages/sections/src/target/MolecularStructure/index.ts b/packages/sections/src/target/MolecularStructure/index.ts index 73654e8ce..81de4c66f 100644 --- a/packages/sections/src/target/MolecularStructure/index.ts +++ b/packages/sections/src/target/MolecularStructure/index.ts @@ -5,6 +5,7 @@ export const definition = { id: "molecularStructure", name: "Molecular Structure", shortName: "MS", + category: "Target", hasData: (data: TargetData) => { return data?.proteinIds?.some?.(e => e.source === "uniprot_swissprot") || false; }, diff --git a/packages/sections/src/target/MousePhenotypes/index.ts b/packages/sections/src/target/MousePhenotypes/index.ts index 767458ab3..2887113cd 100644 --- a/packages/sections/src/target/MousePhenotypes/index.ts +++ b/packages/sections/src/target/MousePhenotypes/index.ts @@ -5,6 +5,7 @@ export const definition = { id: "mousePhenotypes", name: "Mouse Phenotypes", shortName: "MP", + category: "Target-Disease", hasData: (data: TargetData) => (data.mousePhenotypes?.length || 0) > 0, }; diff --git a/packages/sections/src/target/OverlappingVariants/index.ts b/packages/sections/src/target/OverlappingVariants/index.ts index 3ea92a256..3e46679ea 100644 --- a/packages/sections/src/target/OverlappingVariants/index.ts +++ b/packages/sections/src/target/OverlappingVariants/index.ts @@ -6,6 +6,7 @@ export const definition = { id, name: "Protein Coding Variants", shortName: "PC", + category: "Target-Variant", hasData: (data: TargetData) => (data.proteinCodingCoordinates?.count || 0) > 0, }; diff --git a/packages/sections/src/target/Pathways/index.ts b/packages/sections/src/target/Pathways/index.ts index 1529caeb5..b60f476a2 100644 --- a/packages/sections/src/target/Pathways/index.ts +++ b/packages/sections/src/target/Pathways/index.ts @@ -5,6 +5,7 @@ export const definition = { id: "pathways", name: "Pathways", shortName: "PW", + category: "Target", hasData: (data: TargetData) => (data.pathways?.length || 0) > 0, }; diff --git a/packages/sections/src/target/Pharmacogenomics/index.ts b/packages/sections/src/target/Pharmacogenomics/index.ts index 4d30804d1..86b603cd0 100644 --- a/packages/sections/src/target/Pharmacogenomics/index.ts +++ b/packages/sections/src/target/Pharmacogenomics/index.ts @@ -5,6 +5,7 @@ export const definition = { id: "pharmacogenetics", name: "Pharmacogenetics", shortName: "PGx", + category: "Drug", hasData: (data: TargetData) => (data.pharmacogenomics?.length || 0) > 0, }; diff --git a/packages/sections/src/target/QTLCredibleSets/index.ts b/packages/sections/src/target/QTLCredibleSets/index.ts index e846ac922..cb74f1e8e 100644 --- a/packages/sections/src/target/QTLCredibleSets/index.ts +++ b/packages/sections/src/target/QTLCredibleSets/index.ts @@ -5,6 +5,7 @@ export const definition = { id: "qtl_credible_sets", name: "molQTL Credible Sets", shortName: "QT", + category: "Target-Variant", // @ts-expect-error TODO: fix this hasData: (data: Target) => (data.credibleSets?.count || 0) > 0, }; diff --git a/packages/sections/src/target/Safety/index.ts b/packages/sections/src/target/Safety/index.ts index 7ba433cf0..24283afba 100644 --- a/packages/sections/src/target/Safety/index.ts +++ b/packages/sections/src/target/Safety/index.ts @@ -5,6 +5,7 @@ export const definition = { id: "safety", name: "Safety", shortName: "S", + category: "Target", hasData: (data: TargetData) => (data.safetyLiabilities?.length || 0) > 0, }; diff --git a/packages/sections/src/target/SubcellularLocation/index.ts b/packages/sections/src/target/SubcellularLocation/index.ts index ac6fe1d42..7a54b6a17 100644 --- a/packages/sections/src/target/SubcellularLocation/index.ts +++ b/packages/sections/src/target/SubcellularLocation/index.ts @@ -5,6 +5,7 @@ export const definition = { id: "subcellularLocation", name: "Subcellular Location", shortName: "SL", + category: "Target", hasData: (data: TargetData) => (data.subcellularLocations?.length || 0) > 0, }; diff --git a/packages/sections/src/target/Tractability/index.ts b/packages/sections/src/target/Tractability/index.ts index 8e04d0e8d..75cf2fe6b 100644 --- a/packages/sections/src/target/Tractability/index.ts +++ b/packages/sections/src/target/Tractability/index.ts @@ -5,6 +5,7 @@ export const definition = { id: "tractability", name: "Tractability", shortName: "TR", + category: "Target", hasData: (data: TargetData) => (data.tractability?.length || 0) > 0, }; diff --git a/packages/sections/src/types/widget.ts b/packages/sections/src/types/widget.ts index e4f0df911..0727e7ddf 100644 --- a/packages/sections/src/types/widget.ts +++ b/packages/sections/src/types/widget.ts @@ -1,4 +1,5 @@ import { ComponentType } from 'react'; +import type { Category } from 'ui'; /** * Generic widget interface for profile sections @@ -11,6 +12,7 @@ export interface Widget { shortName: string; hasData: (data: any) => boolean | undefined; isPrivate?: boolean; + category?: Category | Category[]; }; Summary: ComponentType; getBodyComponent: () => ComponentType; @@ -26,6 +28,7 @@ export interface WidgetDefinition { shortName: string; hasData: (data: any) => boolean | undefined; isPrivate: boolean; + category?: Category | Category[]; } /** diff --git a/packages/sections/src/variant/EVA/index.ts b/packages/sections/src/variant/EVA/index.ts index 72b4df167..62d642569 100644 --- a/packages/sections/src/variant/EVA/index.ts +++ b/packages/sections/src/variant/EVA/index.ts @@ -5,6 +5,7 @@ export const definition = { id: "eva", name: "ClinVar", shortName: "CV", + category: "Disease-Variant", hasData: (data: Variant) => { return ( // @ts-expect-error TODO: fix this diff --git a/packages/sections/src/variant/EnhancerToGenePredictions/index.ts b/packages/sections/src/variant/EnhancerToGenePredictions/index.ts index ef2b8cf8d..28473bc1f 100644 --- a/packages/sections/src/variant/EnhancerToGenePredictions/index.ts +++ b/packages/sections/src/variant/EnhancerToGenePredictions/index.ts @@ -5,6 +5,7 @@ export const definition = { id: "Enhancer_to_gene_predictions", name: "Enhancer-to-gene predictions", shortName: "EG", + category: "Target-Variant", hasData: (data: Variant) => { return (data.enhancerToGenes?.count || 0) > 0; }, diff --git a/packages/sections/src/variant/GWASCredibleSets/index.ts b/packages/sections/src/variant/GWASCredibleSets/index.ts index 0181d8106..00c8c3b5a 100644 --- a/packages/sections/src/variant/GWASCredibleSets/index.ts +++ b/packages/sections/src/variant/GWASCredibleSets/index.ts @@ -5,6 +5,7 @@ export const definition = { id: "gwas_credible_sets", name: "GWAS Credible Sets", shortName: "GW", + category: "Disease-Variant", // @ts-expect-error TODO: fix this hasData: (data: Variant) => (data.gwasCredibleSets?.count || 0) > 0, }; diff --git a/packages/sections/src/variant/MolecularStructure/index.ts b/packages/sections/src/variant/MolecularStructure/index.ts index 5ed2acd63..238d8e9f1 100644 --- a/packages/sections/src/variant/MolecularStructure/index.ts +++ b/packages/sections/src/variant/MolecularStructure/index.ts @@ -5,6 +5,7 @@ export const definition = { id: "molecular_structure", name: "Molecular Structure", shortName: "MS", + category: "Target-Variant", hasData: (data: Variant) => (data.proteinCodingCoordinates?.count || 0) > 0 && data.proteinCodingCoordinates?.rows?.[0]?.referenceAminoAcid !== "-", diff --git a/packages/sections/src/variant/Pharmacogenomics/index.ts b/packages/sections/src/variant/Pharmacogenomics/index.ts index b14b57a89..ef5e710ab 100644 --- a/packages/sections/src/variant/Pharmacogenomics/index.ts +++ b/packages/sections/src/variant/Pharmacogenomics/index.ts @@ -5,6 +5,7 @@ export const definition = { id: "pharmacogenetics", name: "Pharmacogenetics", shortName: "PGx", + category: "Drug", hasData: (data: Variant) => (data.pharmacogenomics?.length || 0) > 0, }; diff --git a/packages/sections/src/variant/QTLCredibleSets/index.ts b/packages/sections/src/variant/QTLCredibleSets/index.ts index b6e3af747..6d8ce2973 100644 --- a/packages/sections/src/variant/QTLCredibleSets/index.ts +++ b/packages/sections/src/variant/QTLCredibleSets/index.ts @@ -5,6 +5,7 @@ export const definition = { id: "qtl_credible_sets", name: "molQTL Credible Sets", shortName: "QT", + category: "Target-Variant", // @ts-expect-error TODO: fix this hasData: (data: Variant) => (data.qtlCredibleSets?.count || 0) > 0, }; diff --git a/packages/sections/src/variant/UniProtVariants/index.ts b/packages/sections/src/variant/UniProtVariants/index.ts index c8819e357..d6ea91077 100644 --- a/packages/sections/src/variant/UniProtVariants/index.ts +++ b/packages/sections/src/variant/UniProtVariants/index.ts @@ -5,6 +5,7 @@ export const definition = { id: "uniprot_variants", name: "UniProt variants", shortName: "UV", + category: "Disease-Variant", hasData: (data: Variant) => { return ( // @ts-expect-error TODO: fix this diff --git a/packages/sections/src/variant/VariantEffect/index.ts b/packages/sections/src/variant/VariantEffect/index.ts index 5a03bc659..3c05003c1 100644 --- a/packages/sections/src/variant/VariantEffect/index.ts +++ b/packages/sections/src/variant/VariantEffect/index.ts @@ -5,6 +5,7 @@ export const definition = { id: "in_silico_predictors", name: "Variant effect", shortName: "VP", + category: "Target-Variant", hasData: (data: Variant) => (data.variantEffect?.length || 0) > 0, }; diff --git a/packages/sections/src/variant/VariantEffectPredictor/index.ts b/packages/sections/src/variant/VariantEffectPredictor/index.ts index 05501a768..0dd654c1a 100644 --- a/packages/sections/src/variant/VariantEffectPredictor/index.ts +++ b/packages/sections/src/variant/VariantEffectPredictor/index.ts @@ -5,6 +5,7 @@ export const definition = { id: "variant_effect_predictor", name: "Transcript consequences", shortName: "TC", + category: "Target-Variant", hasData: (data: Variant) => (data.transcriptConsequences?.length || 0) > 0, }; diff --git a/packages/ui/src/components/Summary/categoryConfig.ts b/packages/ui/src/components/Summary/categoryConfig.ts new file mode 100644 index 000000000..7ef0304bb --- /dev/null +++ b/packages/ui/src/components/Summary/categoryConfig.ts @@ -0,0 +1,32 @@ +import { + faBullseye, + faDisease, + faPills, + faLink, + faDna, + faVial, + faBook, +} from "@fortawesome/free-solid-svg-icons"; +import type { IconDefinition } from "@fortawesome/fontawesome-svg-core"; + +export const CATEGORIES = [ + "Target", + "Disease", + "Drug", + "Target-Disease", + "Target-Variant", + "Disease-Variant", + "Literature", +] as const; + +export type Category = (typeof CATEGORIES)[number]; + +export const CATEGORY_ICONS: Record = { + Target: faBullseye, + Disease: faDisease, + Drug: faPills, + "Target-Disease": faLink, + "Target-Variant": faDna, + "Disease-Variant": faVial, + Literature: faBook, +}; From 5e47cb4d06d597829d0eea3fd61615caec5cb58c Mon Sep 17 00:00:00 2001 From: Carlos Cruz Date: Tue, 28 Jul 2026 16:23:30 +0100 Subject: [PATCH 2/9] refactor: redesign Summary section as compact, category-filterable chips Replaces the initials avatar ("DG"-style, from createShortName) used in three places - Summary cards, Section item headers, and the sticky nav - with a single CategoryAvatar showing each widget's category icon instead. Summary cards go from a card grid to compact pill chips in a CSS grid, grouped/filterable by category via a chip row (styled to match the AOTF control chips) rather than static per-category headers. Also fixes CredibleSet and Study profile pages: both rendered widget Summary components directly instead of through SummaryRenderer, which broke the grid layout (overlapping chips, since SummaryItem no longer self-wraps in a grid item) and meant those two profiles never got category filters at all. --- .../src/pages/CredibleSetPage/Profile.tsx | 4 +- apps/platform/src/pages/StudyPage/Profile.tsx | 12 +-- packages/ui/src/components/CategoryAvatar.tsx | 50 +++++++++ .../components/Section/SectionItem.styles.ts | 11 +- .../ui/src/components/Section/SectionItem.tsx | 6 +- .../StickyProfileHeader.tsx | 26 ++--- .../components/Summary/SummaryItem.styles.ts | 88 +++++---------- .../ui/src/components/Summary/SummaryItem.tsx | 69 ++++-------- .../components/Summary/SummaryRenderer.tsx | 101 ++++++++++++++++-- packages/ui/src/components/Summary/utils.ts | 16 --- packages/ui/src/index.tsx | 3 + 11 files changed, 208 insertions(+), 178 deletions(-) create mode 100644 packages/ui/src/components/CategoryAvatar.tsx diff --git a/apps/platform/src/pages/CredibleSetPage/Profile.tsx b/apps/platform/src/pages/CredibleSetPage/Profile.tsx index 54ce014ca..0ef08a76e 100644 --- a/apps/platform/src/pages/CredibleSetPage/Profile.tsx +++ b/apps/platform/src/pages/CredibleSetPage/Profile.tsx @@ -74,9 +74,7 @@ function Profile({ studyLocusId, variantId, Icon, externalLinks }: ProfileProps) /> - {/* TODO: remove this once we have a proper variants section. look at the parent prop */} - - + diff --git a/apps/platform/src/pages/StudyPage/Profile.tsx b/apps/platform/src/pages/StudyPage/Profile.tsx index 3f2753158..7d7c6aa72 100644 --- a/apps/platform/src/pages/StudyPage/Profile.tsx +++ b/apps/platform/src/pages/StudyPage/Profile.tsx @@ -6,6 +6,7 @@ import { SectionContainer, StickyProfileHeader, SummaryContainer, + SummaryRenderer, SectionLoader, summaryUtils, } from "ui"; @@ -75,14 +76,9 @@ function Profile({ studyId, studyType, diseases, Icon, externalLinks }: ProfileP /> - {/* TODO: remove this, check the studyType property */} - {studyType === "gwas" && ( - <> - - - - )} - {studyType !== "gwas" && } + diff --git a/packages/ui/src/components/CategoryAvatar.tsx b/packages/ui/src/components/CategoryAvatar.tsx new file mode 100644 index 000000000..32a746782 --- /dev/null +++ b/packages/ui/src/components/CategoryAvatar.tsx @@ -0,0 +1,50 @@ +import { Avatar, type SxProps, type Theme, Tooltip } from "@mui/material"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { CATEGORY_ICONS, type Category } from "./Summary/categoryConfig"; + +type CategoryAvatarProps = { + definition: { category?: Category | Category[] }; + hasData: boolean; + error?: boolean; + size?: number; + shape?: "circular" | "square"; + className?: string; + sx?: SxProps; +}; + +function CategoryAvatar({ + definition, + hasData, + error, + size = 40, + shape = "circular", + className, + sx, +}: CategoryAvatarProps) { + const categories: Category[] = definition.category + ? ([] as Category[]).concat(definition.category) + : []; + const primary = categories[0]; + const icon = primary ? CATEGORY_ICONS[primary] : undefined; + + const avatar = ( + + {icon && } + + ); + + return categories.length > 0 ? {avatar} : avatar; +} + +export default CategoryAvatar; diff --git a/packages/ui/src/components/Section/SectionItem.styles.ts b/packages/ui/src/components/Section/SectionItem.styles.ts index a3081b73d..ef9d02e49 100644 --- a/packages/ui/src/components/Section/SectionItem.styles.ts +++ b/packages/ui/src/components/Section/SectionItem.styles.ts @@ -1,4 +1,4 @@ -import { Avatar, Box, CardContent, styled, Typography } from "@mui/material"; +import { Box, CardContent, styled, Typography } from "@mui/material"; export const CardHeaderContainer = styled(Box)({ display: "flex", @@ -13,15 +13,6 @@ export const StyledCardContent = styled(CardContent)(({ theme }) => ({ minHeight: 36, })); -// NOTE: under the old JSS makeStyles, `avatarHasData`'s `!important` background -// always won over `avatarError`'s non-important one (both were applied together -// whenever hasData was true), so the avatar never actually turned "error" colored. -// Preserved as-is here rather than fixed, to keep this migration a pure refactor. -export const StyledAvatar = styled(Avatar)(({ theme }) => ({ - color: "white", - backgroundColor: theme.palette.primary.dark, -})); - export const StyledTitle = styled("div", { shouldForwardProp: (prop) => prop !== "error", })<{ error?: boolean }>(({ theme, error }) => ({ diff --git a/packages/ui/src/components/Section/SectionItem.tsx b/packages/ui/src/components/Section/SectionItem.tsx index 948e32896..72d005773 100644 --- a/packages/ui/src/components/Section/SectionItem.tsx +++ b/packages/ui/src/components/Section/SectionItem.tsx @@ -2,15 +2,14 @@ import { Box, Card, Divider, GridLegacy, Skeleton } from "@mui/material"; import { VIEW } from "@ot/constants"; import { type ReactNode, useEffect, useState } from "react"; import { Element } from "react-scroll"; +import CategoryAvatar from "../CategoryAvatar"; import ErrorBoundary from "../ErrorBoundary"; import PartnerLockIcon from "../PartnerLockIcon"; import { SummaryLoader } from "../PublicationsDrawer"; -import { createShortName } from "../Summary/utils"; import SectionError from "./SectionError"; import { CardHeaderContainer, NoData, - StyledAvatar, StyledCardContent, StyledChip, StyledDescription, @@ -57,7 +56,6 @@ function SectionItem({ defaultView = VIEW.table, }: SectionItemProps): ReactNode { const { loading, error, data } = request; - const shortName = createShortName(definition); let hasData = false; const [selectedView, setSelectedView] = useState(defaultView); const [showDelayLoadingMessage, setShowDelayLoadingMessage] = useState(false); @@ -106,7 +104,7 @@ function SectionItem({ {/* AVATAR */} - {shortName} + {/* HEADER, SUB-HEADER & CHIP */} boolean | undefined; isPrivate?: boolean; + category?: Category | Category[]; }; type StickyProfileHeaderProps = { @@ -32,13 +34,6 @@ type StickyProfileHeaderProps = { widgets: { definition: WidgetDefinition }[]; }; -function avatarSx(hasData: boolean) { - return { - bgcolor: hasData ? "primary.dark" : "grey.300", - color: hasData ? "white" : "grey.600", - }; -} - function StickyProfileHeader({ title, Icon, @@ -121,9 +116,7 @@ function StickyProfileHeader({ setAnchorEl(event.currentTarget)}> - - {createShortName(activeWidget.definition)} - + {activeWidget.definition.name} @@ -149,9 +142,12 @@ function StickyProfileHeader({ disabled={!hasData} onClick={() => handleSelect(widget.definition.id)} > - - {createShortName(widget.definition)} - + {widget.definition.name} ); diff --git a/packages/ui/src/components/Summary/SummaryItem.styles.ts b/packages/ui/src/components/Summary/SummaryItem.styles.ts index 7a99077b1..adf7f7da4 100644 --- a/packages/ui/src/components/Summary/SummaryItem.styles.ts +++ b/packages/ui/src/components/Summary/SummaryItem.styles.ts @@ -1,72 +1,36 @@ -import { Avatar, Card, styled, Typography } from "@mui/material"; +import { Box, styled, Typography } from "@mui/material"; -export const StyledAvatar = styled(Avatar, { - shouldForwardProp: (prop) => prop !== "hasData" && prop !== "error", +export const StyledChip = styled(Box, { + shouldForwardProp: prop => prop !== "hasData" && prop !== "error", })<{ hasData?: boolean; error?: boolean }>(({ theme, hasData, error }) => ({ - color: "white", - backgroundColor: error - ? theme.palette.secondary.main - : hasData - ? theme.palette.primary.dark - : theme.palette.grey[300], + display: "inline-flex", + alignItems: "center", + gap: "1.1rem", + padding: "0 1.5rem 0 0", + borderRadius: theme.shape.borderRadius * 2, + overflow: "hidden", + maxWidth: "100%", + backgroundColor: theme.palette.common.white, + border: `1px solid ${theme.palette.grey[300]}`, + ...(hasData && { + cursor: "pointer", + "&:hover": { + backgroundColor: theme.palette.primary.dark, + "& .summaryChipLabel": { color: theme.palette.common.white }, + }, + }), })); -export const StyledTitle = styled(Typography, { - shouldForwardProp: (prop) => prop !== "hasData" && prop !== "error", +export const StyledLabel = styled(Typography, { + shouldForwardProp: prop => prop !== "hasData" && prop !== "error", })<{ hasData?: boolean; error?: boolean }>(({ theme, hasData, error }) => ({ - wordBreak: "break-word", + fontSize: "0.875rem", + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis", color: error - ? theme.palette.secondary.main + ? theme.palette.error.dark : hasData ? theme.palette.text.primary : theme.palette.grey[500], })); - -export const StyledSubtitle = styled(Typography, { - shouldForwardProp: (prop) => prop !== "hasData", -})<{ hasData?: boolean }>(({ theme, hasData }) => ({ - color: hasData ? theme.palette.text.primary : theme.palette.grey[500], -})); - -// NOTE: under the old JSS makeStyles, `subheaderError` was applied unconditionally -// (`[classes.subheaderError]: true`, not tied to the actual `error` value), and its -// rule was declared after `subheaderHasData`'s, so at equal specificity it always -// won. The subheader was therefore always rendered in the "error" color regardless -// of hasData/error state — subheaderHasData had no visible effect on its own. -// Preserved as-is; the hover-white override below still applies (it comes from a -// higher-specificity JSS selector in the original, reproduced here via StyledCard). -export const StyledSubheader = styled(Typography)(({ theme }) => ({ - fontSize: "0.8rem", - fontStyle: "italic", - color: theme.palette.secondary.main, -})); - -// NOTE: the old `cardError` class was referenced in SummaryItem's classNames() call -// but never existed in makeStyles, so it was always a no-op (JSS silently applied -// no class). No `error` prop is carried here for the same reason. -export const StyledCard = styled(Card, { - shouldForwardProp: (prop) => prop !== "hasData", -})<{ hasData?: boolean }>(({ theme, hasData }) => ({ - height: "100%", - display: "flex", - flexDirection: "row", - ...(hasData && { - cursor: "pointer", - "&:hover": { - transition: "background-color ease-in-out 300ms", - backgroundColor: theme.palette.primary.dark, - }, - // NOTE: this project's Vite build has no @emotion/babel-plugin configured, so - // emotion's `${StyledComponent}` selector-interpolation feature (which needs that - // plugin to tag components with a stable target class) silently resolves to - // "no_component_selector" instead of a real selector. Targeting plain, manually - // assigned class names below instead, which works without the plugin. - "&:hover .summaryItemTitle": { color: "white" }, - "&:hover .summaryItemSubtitle": { color: "white" }, - "&:hover .summaryItemSubheader": { color: "white" }, - "&:hover .summaryItemAvatar": { - color: theme.palette.primary.dark, - backgroundColor: "white !important", - }, - }), -})); diff --git a/packages/ui/src/components/Summary/SummaryItem.tsx b/packages/ui/src/components/Summary/SummaryItem.tsx index f499b7571..29023bb59 100644 --- a/packages/ui/src/components/Summary/SummaryItem.tsx +++ b/packages/ui/src/components/Summary/SummaryItem.tsx @@ -1,16 +1,9 @@ -import { CardHeader, GridLegacy, LinearProgress, Skeleton } from "@mui/material"; import { useNavigate } from "react-router"; import { scroller } from "react-scroll"; +import CategoryAvatar from "../CategoryAvatar"; import PartnerLockIcon from "../PartnerLockIcon"; import { SCROLL_OFFSET } from "../Section/scrollOffset"; -import { - StyledAvatar, - StyledCard, - StyledSubheader, - StyledSubtitle, - StyledTitle, -} from "./SummaryItem.styles"; -import { createShortName } from "./utils"; +import { StyledChip, StyledLabel } from "./SummaryItem.styles"; function SummaryItem({ definition, @@ -22,7 +15,6 @@ function SummaryItem({ subText?: React.ReactNode; }) { const { loading, error, data } = request; - const shortName = createShortName(definition); const hasData = !loading && !error && data && definition.hasData(data); const navigate = useNavigate(); @@ -37,46 +29,25 @@ function SummaryItem({ }; return ( - - + - - {shortName} - - } - title={ - <> - - {loading && } - {!loading && definition.name} {definition.isPrivate ? : null} - - {subText ? ( - - {subText} - - ) : null} - - - {error && "An error occurred while loading this section"} - - - } - /> - {loading && } - - + error={!!error} + size={38} + shape="square" + /> + + {definition.name} + + {definition.isPrivate && } + ); } diff --git a/packages/ui/src/components/Summary/SummaryRenderer.tsx b/packages/ui/src/components/Summary/SummaryRenderer.tsx index 9635dd237..727a2d0a7 100644 --- a/packages/ui/src/components/Summary/SummaryRenderer.tsx +++ b/packages/ui/src/components/Summary/SummaryRenderer.tsx @@ -1,6 +1,11 @@ +import { Box, GridLegacy } from "@mui/material"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useMemo, useState } from "react"; import type { Widget } from "sections"; import { v1 } from "uuid"; import usePermissions from "../../hooks/usePermissions"; +import Chip from "../Chip/Chip"; +import { CATEGORIES, CATEGORY_ICONS, type Category } from "./categoryConfig"; type SummaryRendererProps = { widgets: Widget[]; @@ -8,20 +13,94 @@ type SummaryRendererProps = { keyPrefix?: string; }; +function primaryCategory(widget: Widget): Category | undefined { + const category = widget.definition.category; + return Array.isArray(category) ? category[0] : category; +} + function SummaryRenderer({ widgets, useKeys = true, keyPrefix = "summary" }: SummaryRendererProps) { const { isPartnerPreview } = usePermissions(); + const [activeCategory, setActiveCategory] = useState("All"); + + const visibleWidgets = widgets.filter( + widget => !widget.definition.isPrivate || isPartnerPreview + ); + + // Stable category order so the flat "All" view still visually clusters by category. + const sortedWidgets = useMemo(() => { + const rank = (widget: Widget) => { + const category = primaryCategory(widget); + return category ? CATEGORIES.indexOf(category) : CATEGORIES.length; + }; + return [...visibleWidgets].sort((a, b) => rank(a) - rank(b)); + }, [visibleWidgets]); + + const presentCategories = CATEGORIES.filter(category => + visibleWidgets.some(widget => primaryCategory(widget) === category) + ); + + const filteredWidgets = + activeCategory === "All" + ? sortedWidgets + : sortedWidgets.filter(widget => primaryCategory(widget) === activeCategory); + return ( - <> - {widgets.map((widget, index) => { - const Summary = widget.Summary; - const key = useKeys ? `${keyPrefix}-${v1()}` : `${keyPrefix}-${index}`; - // If the widget is private and we are not in partner preview, don't render it - if (widget.definition.isPrivate && !isPartnerPreview) { - return null; - } - return ; - })} - + + {presentCategories.length > 1 && ( + + setActiveCategory("All")} + sx={{ + height: 26, + borderRadius: 2, + ...(activeCategory === "All" && { + bgcolor: "primary.dark", + color: "common.white", + }), + }} + /> + {presentCategories.map(category => ( + + + {category} + + } + clickable + variant="filled" + onClick={() => setActiveCategory(category)} + sx={{ + height: 26, + borderRadius: 2, + ...(activeCategory === category && { + bgcolor: "primary.dark", + color: "common.white", + }), + }} + /> + ))} + + )} + + {filteredWidgets.map((widget, index) => { + const Summary = widget.Summary; + const key = useKeys ? `${keyPrefix}-${v1()}` : `${keyPrefix}-${index}`; + return ; + })} + + ); } diff --git a/packages/ui/src/components/Summary/utils.ts b/packages/ui/src/components/Summary/utils.ts index 0df118d9e..6f92519c5 100644 --- a/packages/ui/src/components/Summary/utils.ts +++ b/packages/ui/src/components/Summary/utils.ts @@ -4,11 +4,6 @@ type SummaryComponent = { fragments?: Record; }; -type ShortNameDefinition = { - shortName?: string; - name: string; -}; - export function createSummaryFragment( sections: SummaryComponent[], entity: string, @@ -44,14 +39,3 @@ export function createSummaryFragment( )} `; } - -export function createShortName(definition: ShortNameDefinition): string { - return ( - definition.shortName || - definition.name - .split(" ") - .slice(0, 2) - .map((w) => w[0].toUpperCase()) - .join("") - ); -} diff --git a/packages/ui/src/index.tsx b/packages/ui/src/index.tsx index 9fe2b31c4..fed229ab7 100644 --- a/packages/ui/src/index.tsx +++ b/packages/ui/src/index.tsx @@ -134,6 +134,7 @@ export { default as OtTable } from "./components/OtTable/OtTable"; export { default as OtTableSSP } from "./components/OtTable/OtTableSSP"; export { default as PageMeta } from "./components/PageMeta"; export { default as PartnerLockIcon } from "./components/PartnerLockIcon"; +export { default as CategoryAvatar } from "./components/CategoryAvatar"; export { default as Popper } from "./components/Popper"; export { default as PrivateRoute } from "./components/PrivateRoute"; export { default as PrivateWrapper } from "./components/PrivateWrapper"; @@ -149,6 +150,8 @@ export { default as SummaryContainer } from "./components/Summary/SummaryContain export { default as SummaryItem } from "./components/Summary/SummaryItem"; export { default as SummaryRenderer } from "./components/Summary/SummaryRenderer"; export * as summaryUtils from "./components/Summary/utils"; +export { CATEGORIES, CATEGORY_ICONS } from "./components/Summary/categoryConfig"; +export type { Category } from "./components/Summary/categoryConfig"; export { default as SummaryStatsTable } from "./components/SummaryStatsTable"; export { default as StickyProfileHeader } from "./components/StickyProfileHeader"; export { PROFILE_TABS_SENTINEL_ID } from "./components/Section/scrollOffset"; From 434be2024907c1f89d6c293fa0592c43d837ecac Mon Sep 17 00:00:00 2001 From: Carlos Cruz Date: Wed, 29 Jul 2026 09:22:22 +0100 Subject: [PATCH 3/9] style: polish category chips, section item headers, and genomic location badges - Summary filter chips: fixed hover reverting to grey, active color now matches app blue, no borders (AOTF style), smaller font, kept widget display order instead of regrouping by category - Summary/Section item icons: transparent background with colored icon (CategoryAvatar filled=false) instead of a filled square, consistent border-radius between Summary chips and Section item cards - Section item header: tighter padding, smaller icon - GenomicLocation: rebuilt on shared chip styling, grey-300 fill/border matching the Summary chips - Added a "Core essential gene" chip to TargetPage's ProfileHeader, styled like an active filter chip, shown side-by-side with GenomicLocation --- .../src/pages/TargetPage/ProfileHeader.tsx | 48 ++++++++------- packages/ui/src/components/CategoryAvatar.tsx | 8 ++- .../ui/src/components/GenomicLocation.tsx | 59 +++++++++++-------- .../components/Section/SectionItem.styles.ts | 2 +- .../ui/src/components/Section/SectionItem.tsx | 9 ++- .../components/Summary/SummaryItem.styles.ts | 6 +- .../ui/src/components/Summary/SummaryItem.tsx | 2 + .../components/Summary/SummaryRenderer.tsx | 36 ++++++----- 8 files changed, 103 insertions(+), 67 deletions(-) diff --git a/apps/platform/src/pages/TargetPage/ProfileHeader.tsx b/apps/platform/src/pages/TargetPage/ProfileHeader.tsx index 3781da0ba..b4e37796e 100644 --- a/apps/platform/src/pages/TargetPage/ProfileHeader.tsx +++ b/apps/platform/src/pages/TargetPage/ProfileHeader.tsx @@ -5,6 +5,7 @@ import { Field, Tooltip, Box, + Chip, } from "ui"; import { useTheme } from "@mui/material/styles"; import TargetDescription from "./TargetDescription"; @@ -90,28 +91,31 @@ function ProfileHeader() { descriptions={targetDescription} targetId={data?.target.id} /> - {data?.target.genomicLocation && ( - - )} - {geneInfo - .filter(gi => gi.isVisible) - .map(e => ( - theme.palette.grey[600], - border: theme => `1px solid ${theme.palette.grey[600]}`, - borderRadius: "5px", - width: "min-content", - mt: 1, - typography: "body2", - }} - > - {e.label} - - ))} + + {geneInfo + .filter(gi => gi.isVisible) + .map(e => ( + + + + ))} + {data?.target.genomicLocation && ( + + )} + {synonyms} diff --git a/packages/ui/src/components/CategoryAvatar.tsx b/packages/ui/src/components/CategoryAvatar.tsx index 32a746782..963bcf526 100644 --- a/packages/ui/src/components/CategoryAvatar.tsx +++ b/packages/ui/src/components/CategoryAvatar.tsx @@ -8,6 +8,7 @@ type CategoryAvatarProps = { error?: boolean; size?: number; shape?: "circular" | "square"; + filled?: boolean; className?: string; sx?: SxProps; }; @@ -18,6 +19,7 @@ function CategoryAvatar({ error, size = 40, shape = "circular", + filled = true, className, sx, }: CategoryAvatarProps) { @@ -26,6 +28,7 @@ function CategoryAvatar({ : []; const primary = categories[0]; const icon = primary ? CATEGORY_ICONS[primary] : undefined; + const stateColor = error ? "secondary.main" : hasData ? "primary.dark" : "grey.300"; const avatar = ( diff --git a/packages/ui/src/components/GenomicLocation.tsx b/packages/ui/src/components/GenomicLocation.tsx index 99b24d0bb..6ab6a7e89 100644 --- a/packages/ui/src/components/GenomicLocation.tsx +++ b/packages/ui/src/components/GenomicLocation.tsx @@ -25,7 +25,7 @@ const GenomicLocation: React.FC = ({ theme.palette.grey[600], }} @@ -38,32 +38,39 @@ const GenomicLocation: React.FC = ({ } return ( - + - theme.palette.grey[600], - border: (theme) => `1px solid ${theme.palette.grey[600]}`, - p: "1px 5px", - color: "white", - borderRadius: "5px 0 0 5px", - }} - > - {build} - - theme.palette.grey[600], - border: (theme) => `1px solid ${theme.palette.grey[600]}`, - borderRadius: "0 5px 5px 0", - }} - > - {location} + + theme.palette.grey[300], + border: (theme) => `1px solid ${theme.palette.grey[300]}`, + px: "8px", + py: "2px", + color: (theme) => theme.palette.grey[700], + fontSize: "0.875rem", + borderRadius: (theme) => `${theme.shape.borderRadius}px 0 0 ${theme.shape.borderRadius}px`, + }} + > + {build} + + `1px solid ${theme.palette.grey[300]}`, + borderLeft: "none", + px: "8px", + py: "2px", + color: (theme) => theme.palette.grey[700], + fontSize: "0.875rem", + borderRadius: (theme) => `0 ${theme.shape.borderRadius}px ${theme.shape.borderRadius}px 0`, + }} + > + {location} + diff --git a/packages/ui/src/components/Section/SectionItem.styles.ts b/packages/ui/src/components/Section/SectionItem.styles.ts index ef9d02e49..0d7e93f60 100644 --- a/packages/ui/src/components/Section/SectionItem.styles.ts +++ b/packages/ui/src/components/Section/SectionItem.styles.ts @@ -5,7 +5,7 @@ export const CardHeaderContainer = styled(Box)({ alignItems: "center", justifyContent: "space-between", gap: "1rem", - padding: "1rem", + padding: "0.75rem 1rem", }); export const StyledCardContent = styled(CardContent)(({ theme }) => ({ diff --git a/packages/ui/src/components/Section/SectionItem.tsx b/packages/ui/src/components/Section/SectionItem.tsx index 72d005773..2f24a7e65 100644 --- a/packages/ui/src/components/Section/SectionItem.tsx +++ b/packages/ui/src/components/Section/SectionItem.tsx @@ -104,7 +104,14 @@ function SectionItem({ {/* AVATAR */} - + {/* HEADER, SUB-HEADER & CHIP */} (({ theme, hasData, error }) => ({ display: "inline-flex", alignItems: "center", + height: "48px", gap: "1.1rem", padding: "0 1.5rem 0 0", - borderRadius: theme.shape.borderRadius * 2, + borderRadius: theme.shape.borderRadius, overflow: "hidden", maxWidth: "100%", backgroundColor: theme.palette.common.white, border: `1px solid ${theme.palette.grey[300]}`, + transition: "background-color 0.25s ease, border-color 0.25s ease", ...(hasData && { cursor: "pointer", "&:hover": { backgroundColor: theme.palette.primary.dark, "& .summaryChipLabel": { color: theme.palette.common.white }, + "& .summaryChipIcon": { color: theme.palette.common.white }, }, }), })); @@ -28,6 +31,7 @@ export const StyledLabel = styled(Typography, { whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", + transition: "color 0.25s ease", color: error ? theme.palette.error.dark : hasData diff --git a/packages/ui/src/components/Summary/SummaryItem.tsx b/packages/ui/src/components/Summary/SummaryItem.tsx index 29023bb59..3b8efb770 100644 --- a/packages/ui/src/components/Summary/SummaryItem.tsx +++ b/packages/ui/src/components/Summary/SummaryItem.tsx @@ -37,11 +37,13 @@ function SummaryItem({ sx={{ opacity: loading ? 0.6 : 1 }} > {definition.name} diff --git a/packages/ui/src/components/Summary/SummaryRenderer.tsx b/packages/ui/src/components/Summary/SummaryRenderer.tsx index 727a2d0a7..da7e540d2 100644 --- a/packages/ui/src/components/Summary/SummaryRenderer.tsx +++ b/packages/ui/src/components/Summary/SummaryRenderer.tsx @@ -1,6 +1,6 @@ import { Box, GridLegacy } from "@mui/material"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useMemo, useState } from "react"; +import { useState } from "react"; import type { Widget } from "sections"; import { v1 } from "uuid"; import usePermissions from "../../hooks/usePermissions"; @@ -26,28 +26,21 @@ function SummaryRenderer({ widgets, useKeys = true, keyPrefix = "summary" }: Sum widget => !widget.definition.isPrivate || isPartnerPreview ); - // Stable category order so the flat "All" view still visually clusters by category. - const sortedWidgets = useMemo(() => { - const rank = (widget: Widget) => { - const category = primaryCategory(widget); - return category ? CATEGORIES.indexOf(category) : CATEGORIES.length; - }; - return [...visibleWidgets].sort((a, b) => rank(a) - rank(b)); - }, [visibleWidgets]); - const presentCategories = CATEGORIES.filter(category => visibleWidgets.some(widget => primaryCategory(widget) === category) ); + // Keep the same order the widgets are displayed in elsewhere on the page + // (nav, body) rather than regrouping by category. const filteredWidgets = activeCategory === "All" - ? sortedWidgets - : sortedWidgets.filter(widget => primaryCategory(widget) === activeCategory); + ? visibleWidgets + : visibleWidgets.filter(widget => primaryCategory(widget) === activeCategory); return ( {presentCategories.length > 1 && ( - + @@ -77,9 +75,14 @@ function SummaryRenderer({ widgets, useKeys = true, keyPrefix = "summary" }: Sum sx={{ height: 26, borderRadius: 2, + fontSize: "0.7rem", ...(activeCategory === category && { bgcolor: "primary.dark", color: "common.white", + "&:hover": { + bgcolor: "secondary.main", + color: "common.white", + }, }), }} /> @@ -89,7 +92,12 @@ function SummaryRenderer({ widgets, useKeys = true, keyPrefix = "summary" }: Sum Date: Wed, 29 Jul 2026 17:55:04 +0100 Subject: [PATCH 4/9] feat: sync summary category filter to URL and gate sections/nav by it - New SummaryCategoryContext, URL-synced via useStateParams (?category=), shared across Summary chips, section bodies, and the sticky nav - SectionsRenderer hides full-body sections outside the active category - StickyProfileHeader nav/active-section tracking filtered to match - Wired into all 7 profile pages; Study/CredibleSet pages also gate their manually-rendered sections (shared trait studies, QTL/GWAS credible sets, variants) against the same filter --- .../src/pages/CredibleSetPage/Profile.tsx | 69 ++++++++++----- .../src/pages/DiseasePage/Profile.tsx | 27 +++--- apps/platform/src/pages/DrugPage/Profile.tsx | 27 +++--- .../src/pages/EvidencePage/Profile.tsx | 15 ++-- apps/platform/src/pages/StudyPage/Profile.tsx | 83 +++++++++++++------ .../platform/src/pages/TargetPage/Profile.tsx | 27 +++--- .../src/pages/VariantPage/Profile.tsx | 32 ++++--- .../components/Section/SectionsRenderer.tsx | 6 ++ .../StickyProfileHeader.tsx | 24 ++++-- .../Summary/SummaryCategoryContext.tsx | 45 ++++++++++ .../components/Summary/SummaryRenderer.tsx | 15 ++-- .../src/components/Summary/categoryConfig.ts | 7 ++ packages/ui/src/index.tsx | 4 +- 13 files changed, 263 insertions(+), 118 deletions(-) create mode 100644 packages/ui/src/components/Summary/SummaryCategoryContext.tsx diff --git a/apps/platform/src/pages/CredibleSetPage/Profile.tsx b/apps/platform/src/pages/CredibleSetPage/Profile.tsx index 0ef08a76e..7fc09afb3 100644 --- a/apps/platform/src/pages/CredibleSetPage/Profile.tsx +++ b/apps/platform/src/pages/CredibleSetPage/Profile.tsx @@ -3,11 +3,14 @@ import { PlatformApiProvider, SectionContainer, StickyProfileHeader, + SummaryCategoryProvider, SummaryContainer, summaryUtils, SummaryRenderer, SectionsRenderer, SectionLoader, + primaryCategory, + useSummaryCategory, } from "ui"; import ProfileHeader from "./ProfileHeader"; @@ -58,6 +61,37 @@ const CREDIBLE_SET_PROFILE_QUERY = gql` const VariantsSection = CredibleSet.Variants.getBodyComponent(); +type CredibleSetSectionsProps = { + studyLocusId: string; + variantId: string; +}; + +// Manual (non-SectionsRenderer) Variants section, gated by the same category +// filter as SectionsRenderer so it hides/shows consistently with it. +function CredibleSetSections({ studyLocusId, variantId }: CredibleSetSectionsProps) { + const { activeCategory } = useSummaryCategory(); + const variantsActive = + activeCategory === "All" || + primaryCategory(CredibleSet.Variants.definition) === activeCategory; + + return ( + <> + {/* TODO: remove this once we have a proper variants section. look at the parent prop */} + {variantsActive && ( + }> + + + )} + + + ); +} + function Profile({ studyLocusId, variantId, Icon, externalLinks }: ProfileProps) { return ( - + + - - - + + + - - {/* TODO: remove this once we have a proper variants section. look at the parent prop */} - }> - - - - + + + + ); } diff --git a/apps/platform/src/pages/DiseasePage/Profile.tsx b/apps/platform/src/pages/DiseasePage/Profile.tsx index 3b4b529f2..6f99a4186 100644 --- a/apps/platform/src/pages/DiseasePage/Profile.tsx +++ b/apps/platform/src/pages/DiseasePage/Profile.tsx @@ -5,6 +5,7 @@ import { PlatformApiProvider, SectionContainer, StickyProfileHeader, + SummaryCategoryProvider, SummaryContainer, SectionsRenderer, SummaryRenderer, @@ -67,19 +68,21 @@ function Profile({ efoId, name, Icon, externalLinks }: ProfileProps) { }} > - - - - + + + + + - - - + + + + ); } diff --git a/apps/platform/src/pages/DrugPage/Profile.tsx b/apps/platform/src/pages/DrugPage/Profile.tsx index 099217aad..5a57db09f 100644 --- a/apps/platform/src/pages/DrugPage/Profile.tsx +++ b/apps/platform/src/pages/DrugPage/Profile.tsx @@ -2,6 +2,7 @@ import { PlatformApiProvider, SectionContainer, StickyProfileHeader, + SummaryCategoryProvider, SummaryContainer, SectionsRenderer, SummaryRenderer, @@ -57,18 +58,20 @@ function Profile({ chemblId, name, Icon, externalLinks }: ProfileProps) { return ( - - - - - - - + + + + + + + + + ); } diff --git a/apps/platform/src/pages/EvidencePage/Profile.tsx b/apps/platform/src/pages/EvidencePage/Profile.tsx index 6a573c46b..b401c96ca 100644 --- a/apps/platform/src/pages/EvidencePage/Profile.tsx +++ b/apps/platform/src/pages/EvidencePage/Profile.tsx @@ -2,6 +2,7 @@ import { gql } from "@apollo/client"; import { PlatformApiProvider, SectionContainer, + SummaryCategoryProvider, SummaryContainer, summaryUtils, SummaryRenderer, @@ -99,13 +100,15 @@ function Profile({ > - - - + + + + - - - + + + + ); } diff --git a/apps/platform/src/pages/StudyPage/Profile.tsx b/apps/platform/src/pages/StudyPage/Profile.tsx index 7d7c6aa72..ce12d65e4 100644 --- a/apps/platform/src/pages/StudyPage/Profile.tsx +++ b/apps/platform/src/pages/StudyPage/Profile.tsx @@ -5,10 +5,13 @@ import { PlatformApiProvider, SectionContainer, StickyProfileHeader, + SummaryCategoryProvider, SummaryContainer, SummaryRenderer, SectionLoader, summaryUtils, + primaryCategory, + useSummaryCategory, } from "ui"; import { Study } from "sections"; import ProfileHeader from "./StudyProfileHeader"; @@ -55,6 +58,44 @@ type ProfileProps = { externalLinks?: ReactNode; }; +type StudySectionsProps = { + studyId: string; + studyType: string; + diseaseIds: string[]; +}; + +// Manual (non-SectionsRenderer) section bodies, gated by the same category +// filter as SectionsRenderer so they hide/show consistently with it. +function StudySections({ studyId, studyType, diseaseIds }: StudySectionsProps) { + const { activeCategory } = useSummaryCategory(); + const isActive = (widget: { definition: Parameters[0] }) => + activeCategory === "All" || primaryCategory(widget.definition) === activeCategory; + + return ( + <> + {studyType === "gwas" && ( + <> + {isActive(Study.GWASCredibleSets) && ( + }> + + + )} + {isActive(Study.SharedTraitStudies) && ( + }> + + + )} + + )} + {studyType !== "gwas" && isActive(Study.QTLCredibleSets) && ( + }> + + + )} + + ); +} + function Profile({ studyId, studyType, diseases, Icon, externalLinks }: ProfileProps) { const diseaseIds = diseases?.map(d => d.id) || []; @@ -68,36 +109,24 @@ function Profile({ studyId, studyType, diseases, Icon, externalLinks }: ProfileP }} > - - - - + - - - {studyType === "gwas" && ( - <> - }> - - - }> - - - - )} - {studyType !== "gwas" && ( - }> - - - )} - + + + + + + + + ); } diff --git a/apps/platform/src/pages/TargetPage/Profile.tsx b/apps/platform/src/pages/TargetPage/Profile.tsx index 8bc029047..56fa639f9 100644 --- a/apps/platform/src/pages/TargetPage/Profile.tsx +++ b/apps/platform/src/pages/TargetPage/Profile.tsx @@ -5,6 +5,7 @@ import { PlatformApiProvider, SectionContainer, StickyProfileHeader, + SummaryCategoryProvider, SummaryContainer, summaryUtils, SectionsRenderer, @@ -70,18 +71,20 @@ function Profile({ ensgId, symbol, Icon, externalLinks }: ProfileProps) { return ( - - - - - - - + + + + + + + + + ); } diff --git a/apps/platform/src/pages/VariantPage/Profile.tsx b/apps/platform/src/pages/VariantPage/Profile.tsx index a49594c2c..8cbf4e17e 100644 --- a/apps/platform/src/pages/VariantPage/Profile.tsx +++ b/apps/platform/src/pages/VariantPage/Profile.tsx @@ -5,6 +5,7 @@ import { PlatformApiProvider, SectionContainer, StickyProfileHeader, + SummaryCategoryProvider, SummaryContainer, summaryUtils, SummaryRenderer, @@ -63,18 +64,25 @@ function Profile({ varId, Icon, externalLinks }: ProfileProps) { variables={{ variantId: varId }} > - - - - - - - + + + + + + + + + ); } diff --git a/packages/ui/src/components/Section/SectionsRenderer.tsx b/packages/ui/src/components/Section/SectionsRenderer.tsx index 3f3ce991d..11dbdfdaa 100644 --- a/packages/ui/src/components/Section/SectionsRenderer.tsx +++ b/packages/ui/src/components/Section/SectionsRenderer.tsx @@ -1,6 +1,8 @@ import { Suspense, useMemo } from "react"; import type { Widget } from "sections"; import usePermissions from "../../hooks/usePermissions"; +import { primaryCategory } from "../Summary/categoryConfig"; +import { useSummaryCategory } from "../Summary/SummaryCategoryContext"; import SectionLoader from "./SectionLoader"; type SectionsRendererProps = { @@ -12,6 +14,7 @@ type SectionsRendererProps = { function SectionsRenderer({ id, label, entity, widgets }: SectionsRendererProps) { const { isPartnerPreview } = usePermissions(); + const { activeCategory } = useSummaryCategory(); // widget.getBodyComponent() creates a brand-new React.lazy() wrapper on every // call, so it must only be invoked once per widget. Calling it inline during @@ -31,6 +34,9 @@ function SectionsRenderer({ id, label, entity, widgets }: SectionsRendererProps) if (isPrivate && !isPartnerPreview) { return null; } + if (activeCategory !== "All" && primaryCategory(widget.definition) !== activeCategory) { + return null; + } return ( }> diff --git a/packages/ui/src/components/StickyProfileHeader/StickyProfileHeader.tsx b/packages/ui/src/components/StickyProfileHeader/StickyProfileHeader.tsx index 99d34964b..cec34fc9f 100644 --- a/packages/ui/src/components/StickyProfileHeader/StickyProfileHeader.tsx +++ b/packages/ui/src/components/StickyProfileHeader/StickyProfileHeader.tsx @@ -8,7 +8,8 @@ import { useNavigate } from "react-router"; import { scroller } from "react-scroll"; import usePlatformApi from "../../hooks/usePlatformApi"; import CategoryAvatar from "../CategoryAvatar"; -import type { Category } from "../Summary/categoryConfig"; +import { primaryCategory, type Category } from "../Summary/categoryConfig"; +import { useSummaryCategory } from "../Summary/SummaryCategoryContext"; import { PROFILE_TABS_SENTINEL_ID, SCROLL_OFFSET, @@ -43,13 +44,26 @@ function StickyProfileHeader({ const theme = useTheme(); const navigate = useNavigate(); const { data, entity } = usePlatformApi(); + const { activeCategory } = useSummaryCategory(); const [anchorEl, setAnchorEl] = useState(null); const [filterText, setFilterText] = useState(""); const [isVisible, setIsVisible] = useState(false); - const ids = useMemo(() => widgets.map((widget) => widget.definition.id), [widgets]); + // Keep the nav in sync with the category filter applied to the Summary/ + // Section widgets below - a hidden widget shouldn't be selectable or + // trackable as the "active" one. + const categoryWidgets = useMemo( + () => + activeCategory === "All" + ? widgets + : widgets.filter((widget) => primaryCategory(widget.definition) === activeCategory), + [widgets, activeCategory] + ); + + const ids = useMemo(() => categoryWidgets.map((widget) => widget.definition.id), [categoryWidgets]); const activeId = useActiveSection(ids); - const activeWidget = widgets.find((widget) => widget.definition.id === activeId) ?? widgets[0]; + const activeWidget = + categoryWidgets.find((widget) => widget.definition.id === activeId) ?? categoryWidgets[0]; const entityData = data?.[entity]; @@ -79,7 +93,7 @@ function StickyProfileHeader({ window.history.replaceState(null, "", `#${activeId}`); }, [isVisible, activeId]); - const filteredWidgets = widgets.filter((widget) => + const filteredWidgets = categoryWidgets.filter((widget) => widget.definition.name.toLowerCase().includes(filterText.toLowerCase()) ); @@ -90,7 +104,7 @@ function StickyProfileHeader({ setFilterText(""); }; - if (widgets.length === 0) return null; + if (categoryWidgets.length === 0) return null; const activeHasData = entityData ? !!activeWidget.definition.hasData(entityData) : true; diff --git a/packages/ui/src/components/Summary/SummaryCategoryContext.tsx b/packages/ui/src/components/Summary/SummaryCategoryContext.tsx new file mode 100644 index 000000000..daf50a2a9 --- /dev/null +++ b/packages/ui/src/components/Summary/SummaryCategoryContext.tsx @@ -0,0 +1,45 @@ +import { createContext, useContext, useMemo, type ReactNode } from "react"; +import useStateParams from "../../hooks/useStateParams"; +import { CATEGORIES, type Category } from "./categoryConfig"; + +export type CategoryFilter = Category | "All"; + +type SummaryCategoryContextValue = { + activeCategory: CategoryFilter; + setActiveCategory: (category: CategoryFilter) => void; +}; + +const SummaryCategoryContext = createContext(undefined); + +function deserializeCategory(value: string): CategoryFilter { + return (CATEGORIES as readonly string[]).includes(value) ? (value as Category) : "All"; +} + +// URL-synced so the current category filter is shareable/deep-linkable and +// survives back/forward navigation, matching the pattern used elsewhere in +// the app (see AssociationsURLContext) rather than a plain useState that +// resets on remount. +function SummaryCategoryProvider({ children }: { children: ReactNode }) { + const [activeCategory, setActiveCategory] = useStateParams( + "All", + "category", + value => value, + deserializeCategory + ); + + const value = useMemo(() => ({ activeCategory, setActiveCategory }), [activeCategory]); + + return ( + {children} + ); +} + +function useSummaryCategory(): SummaryCategoryContextValue { + const context = useContext(SummaryCategoryContext); + if (!context) { + throw new Error("useSummaryCategory must be used within a SummaryCategoryProvider"); + } + return context; +} + +export { SummaryCategoryProvider, useSummaryCategory }; diff --git a/packages/ui/src/components/Summary/SummaryRenderer.tsx b/packages/ui/src/components/Summary/SummaryRenderer.tsx index da7e540d2..b62a69a63 100644 --- a/packages/ui/src/components/Summary/SummaryRenderer.tsx +++ b/packages/ui/src/components/Summary/SummaryRenderer.tsx @@ -1,11 +1,11 @@ import { Box, GridLegacy } from "@mui/material"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useState } from "react"; import type { Widget } from "sections"; import { v1 } from "uuid"; import usePermissions from "../../hooks/usePermissions"; import Chip from "../Chip/Chip"; -import { CATEGORIES, CATEGORY_ICONS, type Category } from "./categoryConfig"; +import { CATEGORIES, CATEGORY_ICONS, primaryCategory } from "./categoryConfig"; +import { useSummaryCategory } from "./SummaryCategoryContext"; type SummaryRendererProps = { widgets: Widget[]; @@ -13,21 +13,16 @@ type SummaryRendererProps = { keyPrefix?: string; }; -function primaryCategory(widget: Widget): Category | undefined { - const category = widget.definition.category; - return Array.isArray(category) ? category[0] : category; -} - function SummaryRenderer({ widgets, useKeys = true, keyPrefix = "summary" }: SummaryRendererProps) { const { isPartnerPreview } = usePermissions(); - const [activeCategory, setActiveCategory] = useState("All"); + const { activeCategory, setActiveCategory } = useSummaryCategory(); const visibleWidgets = widgets.filter( widget => !widget.definition.isPrivate || isPartnerPreview ); const presentCategories = CATEGORIES.filter(category => - visibleWidgets.some(widget => primaryCategory(widget) === category) + visibleWidgets.some(widget => primaryCategory(widget.definition) === category) ); // Keep the same order the widgets are displayed in elsewhere on the page @@ -35,7 +30,7 @@ function SummaryRenderer({ widgets, useKeys = true, keyPrefix = "summary" }: Sum const filteredWidgets = activeCategory === "All" ? visibleWidgets - : visibleWidgets.filter(widget => primaryCategory(widget) === activeCategory); + : visibleWidgets.filter(widget => primaryCategory(widget.definition) === activeCategory); return ( diff --git a/packages/ui/src/components/Summary/categoryConfig.ts b/packages/ui/src/components/Summary/categoryConfig.ts index 7ef0304bb..454d6a1fb 100644 --- a/packages/ui/src/components/Summary/categoryConfig.ts +++ b/packages/ui/src/components/Summary/categoryConfig.ts @@ -30,3 +30,10 @@ export const CATEGORY_ICONS: Record = { "Disease-Variant": faVial, Literature: faBook, }; + +export function primaryCategory(definition: { + category?: Category | Category[]; +}): Category | undefined { + const category = definition.category; + return Array.isArray(category) ? category[0] : category; +} diff --git a/packages/ui/src/index.tsx b/packages/ui/src/index.tsx index fed229ab7..d58b5082b 100644 --- a/packages/ui/src/index.tsx +++ b/packages/ui/src/index.tsx @@ -150,8 +150,10 @@ export { default as SummaryContainer } from "./components/Summary/SummaryContain export { default as SummaryItem } from "./components/Summary/SummaryItem"; export { default as SummaryRenderer } from "./components/Summary/SummaryRenderer"; export * as summaryUtils from "./components/Summary/utils"; -export { CATEGORIES, CATEGORY_ICONS } from "./components/Summary/categoryConfig"; +export { CATEGORIES, CATEGORY_ICONS, primaryCategory } from "./components/Summary/categoryConfig"; export type { Category } from "./components/Summary/categoryConfig"; +export { SummaryCategoryProvider, useSummaryCategory } from "./components/Summary/SummaryCategoryContext"; +export type { CategoryFilter } from "./components/Summary/SummaryCategoryContext"; export { default as SummaryStatsTable } from "./components/SummaryStatsTable"; export { default as StickyProfileHeader } from "./components/StickyProfileHeader"; export { PROFILE_TABS_SENTINEL_ID } from "./components/Section/scrollOffset"; From b403cf9b65dd05f51f89a2bc90c2a42fc695638c Mon Sep 17 00:00:00 2001 From: Carlos Cruz Date: Wed, 29 Jul 2026 18:03:26 +0100 Subject: [PATCH 5/9] fix: preserve category search param when navigating to a section hash navigate({ hash }) without an explicit search wipes existing query params, so clicking a Summary chip or sticky-nav item was resetting the ?category= filter back to All. Pass through location.search. --- .../StickyProfileHeader/StickyProfileHeader.tsx | 10 ++++++++-- packages/ui/src/components/Summary/SummaryItem.tsx | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/components/StickyProfileHeader/StickyProfileHeader.tsx b/packages/ui/src/components/StickyProfileHeader/StickyProfileHeader.tsx index cec34fc9f..a72e01c1c 100644 --- a/packages/ui/src/components/StickyProfileHeader/StickyProfileHeader.tsx +++ b/packages/ui/src/components/StickyProfileHeader/StickyProfileHeader.tsx @@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Box, Menu, MenuItem, TextField, Typography } from "@mui/material"; import { useTheme } from "@mui/material/styles"; import { useEffect, useMemo, useState, type ReactNode } from "react"; -import { useNavigate } from "react-router"; +import { useLocation, useNavigate } from "react-router"; import { scroller } from "react-scroll"; import usePlatformApi from "../../hooks/usePlatformApi"; import CategoryAvatar from "../CategoryAvatar"; @@ -43,6 +43,7 @@ function StickyProfileHeader({ }: StickyProfileHeaderProps) { const theme = useTheme(); const navigate = useNavigate(); + const location = useLocation(); const { data, entity } = usePlatformApi(); const { activeCategory } = useSummaryCategory(); const [anchorEl, setAnchorEl] = useState(null); @@ -99,7 +100,12 @@ function StickyProfileHeader({ const handleSelect = (id: string) => { scroller.scrollTo(id, { duration: 500, smooth: true, offset: SCROLL_OFFSET }); - navigate({ hash: `#${id}` }, { replace: true, preventScrollReset: true }); + // Preserve the existing search params (e.g. ?category=) - navigate() + // with only `hash` set would otherwise drop them. + navigate( + { hash: `#${id}`, search: location.search }, + { replace: true, preventScrollReset: true } + ); setAnchorEl(null); setFilterText(""); }; diff --git a/packages/ui/src/components/Summary/SummaryItem.tsx b/packages/ui/src/components/Summary/SummaryItem.tsx index 3b8efb770..d135a56a7 100644 --- a/packages/ui/src/components/Summary/SummaryItem.tsx +++ b/packages/ui/src/components/Summary/SummaryItem.tsx @@ -1,4 +1,4 @@ -import { useNavigate } from "react-router"; +import { useLocation, useNavigate } from "react-router"; import { scroller } from "react-scroll"; import CategoryAvatar from "../CategoryAvatar"; import PartnerLockIcon from "../PartnerLockIcon"; @@ -17,6 +17,7 @@ function SummaryItem({ const { loading, error, data } = request; const hasData = !loading && !error && data && definition.hasData(data); const navigate = useNavigate(); + const location = useLocation(); const handleClickSection = () => { scroller.scrollTo(definition.id, { @@ -25,7 +26,12 @@ function SummaryItem({ smooth: true, offset: SCROLL_OFFSET, }); - navigate({ hash: `#${definition.id}` }, { replace: true, preventScrollReset: true }); + // Preserve the existing search params (e.g. ?category=) - navigate() + // with only `hash` set would otherwise drop them. + navigate( + { hash: `#${definition.id}`, search: location.search }, + { replace: true, preventScrollReset: true } + ); }; return ( From 3e9516807a4ecb149a6ab61e0a47307cc44694d2 Mon Sep 17 00:00:00 2001 From: Carlos Cruz Date: Wed, 29 Jul 2026 18:20:28 +0100 Subject: [PATCH 6/9] fix: stale active-section id on filter change, clear hash on scroll-to-top useActiveSection kept its last activeId even after the caller's ids list changed (e.g. switching the category filter), so it could keep pointing at a now-hidden widget until the next scroll-triggered intersection event. Reset/validate activeId whenever ids changes. Also clear the URL hash once the user scrolls back above the sticky trigger, so a refresh at the top of the page doesn't re-jump into whichever section was last active. Guarded to only kick in after the sticky bar has been visible at least once, so it doesn't strip a deep-linked hash before the page has had a chance to scroll to it. --- .../StickyProfileHeader.tsx | 20 ++++++++++++++++--- packages/ui/src/hooks/useActiveSection.ts | 11 +++++++++- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/components/StickyProfileHeader/StickyProfileHeader.tsx b/packages/ui/src/components/StickyProfileHeader/StickyProfileHeader.tsx index a72e01c1c..8d1aff6d5 100644 --- a/packages/ui/src/components/StickyProfileHeader/StickyProfileHeader.tsx +++ b/packages/ui/src/components/StickyProfileHeader/StickyProfileHeader.tsx @@ -3,7 +3,7 @@ import { faChevronDown } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Box, Menu, MenuItem, TextField, Typography } from "@mui/material"; import { useTheme } from "@mui/material/styles"; -import { useEffect, useMemo, useState, type ReactNode } from "react"; +import { useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import { useLocation, useNavigate } from "react-router"; import { scroller } from "react-scroll"; import usePlatformApi from "../../hooks/usePlatformApi"; @@ -89,9 +89,23 @@ function StickyProfileHeader({ // navigate() re-renders the whole matched route tree on each call, which // is both wasteful and was surfacing an unrelated remount bug in one of // the section bodies. + // + // hasBeenVisible guards the "clear the hash" branch below so it only + // fires once the user has actually scrolled down into the sections at + // least once - otherwise a deep link (page loaded with a hash already in + // the URL) would have its hash stripped in the instant before the + // sentinel's IntersectionObserver reports its first isVisible=true. + const hasBeenVisible = useRef(false); useEffect(() => { - if (!isVisible || !activeId) return; - window.history.replaceState(null, "", `#${activeId}`); + if (isVisible) { + hasBeenVisible.current = true; + if (activeId) window.history.replaceState(null, "", `#${activeId}`); + } else if (hasBeenVisible.current && window.location.hash) { + // Scrolled back above the sticky trigger - clear the section hash so + // a refresh lands at the top of the page instead of re-scrolling + // into whichever section was last active. + window.history.replaceState(null, "", window.location.pathname + window.location.search); + } }, [isVisible, activeId]); const filteredWidgets = categoryWidgets.filter((widget) => diff --git a/packages/ui/src/hooks/useActiveSection.ts b/packages/ui/src/hooks/useActiveSection.ts index 02d6b42d2..d2cf60556 100644 --- a/packages/ui/src/hooks/useActiveSection.ts +++ b/packages/ui/src/hooks/useActiveSection.ts @@ -12,7 +12,16 @@ function useActiveSection(ids: string[]): string | null { const [activeId, setActiveId] = useState(ids[0] ?? null); useEffect(() => { - if (ids.length === 0) return undefined; + if (ids.length === 0) { + setActiveId(null); + return undefined; + } + + // ids changes when the caller re-filters the section list (e.g. a + // category filter) - if the previous activeId fell out of the new + // list, it would otherwise keep pointing at a now-hidden section + // until the next scroll-triggered intersection event. + setActiveId((prev) => (prev && ids.includes(prev) ? prev : ids[0])); const visible = new Set(); const observed = new Set(); From 377dec25b0d48a6a245b4a39295853598fe4ae26 Mon Sep 17 00:00:00 2001 From: Carlos Cruz Date: Wed, 29 Jul 2026 19:11:54 +0100 Subject: [PATCH 7/9] feat: add category filter chips to sticky nav widget selector Extracted the "All" + per-category chip row from SummaryRenderer into a shared CategoryFilterChips component (both read/write the same SummaryCategoryContext), and reused it in StickyProfileHeader's widget dropdown alongside the existing text filter, so category filtering is available and visually consistent in both places. --- .../StickyProfileHeader.tsx | 11 ++- .../Summary/CategoryFilterChips.tsx | 72 +++++++++++++++++++ .../components/Summary/SummaryRenderer.tsx | 58 ++------------- 3 files changed, 86 insertions(+), 55 deletions(-) create mode 100644 packages/ui/src/components/Summary/CategoryFilterChips.tsx diff --git a/packages/ui/src/components/StickyProfileHeader/StickyProfileHeader.tsx b/packages/ui/src/components/StickyProfileHeader/StickyProfileHeader.tsx index 8d1aff6d5..1e5e1bf47 100644 --- a/packages/ui/src/components/StickyProfileHeader/StickyProfileHeader.tsx +++ b/packages/ui/src/components/StickyProfileHeader/StickyProfileHeader.tsx @@ -8,7 +8,8 @@ import { useLocation, useNavigate } from "react-router"; import { scroller } from "react-scroll"; import usePlatformApi from "../../hooks/usePlatformApi"; import CategoryAvatar from "../CategoryAvatar"; -import { primaryCategory, type Category } from "../Summary/categoryConfig"; +import CategoryFilterChips from "../Summary/CategoryFilterChips"; +import { CATEGORIES, primaryCategory, type Category } from "../Summary/categoryConfig"; import { useSummaryCategory } from "../Summary/SummaryCategoryContext"; import { PROFILE_TABS_SENTINEL_ID, @@ -61,6 +62,13 @@ function StickyProfileHeader({ [widgets, activeCategory] ); + // Full (unfiltered) category set so the chip row always shows every + // category available on this page, even while one is active. + const presentCategories = useMemo( + () => CATEGORIES.filter((category) => widgets.some((widget) => primaryCategory(widget.definition) === category)), + [widgets] + ); + const ids = useMemo(() => categoryWidgets.map((widget) => widget.definition.id), [categoryWidgets]); const activeId = useActiveSection(ids); const activeWidget = @@ -167,6 +175,7 @@ function StickyProfileHeader({ onKeyDown={(event) => event.stopPropagation()} sx={{ px: 1.5, pb: 1, width: "100%" }} /> + {filteredWidgets.map((widget) => { const hasData = entityData ? !!widget.definition.hasData(entityData) : true; return ( diff --git a/packages/ui/src/components/Summary/CategoryFilterChips.tsx b/packages/ui/src/components/Summary/CategoryFilterChips.tsx new file mode 100644 index 000000000..9a3dc1443 --- /dev/null +++ b/packages/ui/src/components/Summary/CategoryFilterChips.tsx @@ -0,0 +1,72 @@ +import { Box, type SxProps, type Theme } from "@mui/material"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import Chip from "../Chip/Chip"; +import { CATEGORY_ICONS, type Category } from "./categoryConfig"; +import { useSummaryCategory } from "./SummaryCategoryContext"; + +type CategoryFilterChipsProps = { + categories: Category[]; + sx?: SxProps; +}; + +// Shared "All" + per-category filter chip row, used both in the Summary +// grid and the sticky nav's widget selector so the two stay visually and +// behaviorally identical - both read/write the same SummaryCategoryContext. +function CategoryFilterChips({ categories, sx }: CategoryFilterChipsProps) { + const { activeCategory, setActiveCategory } = useSummaryCategory(); + + if (categories.length <= 1) return null; + + return ( + + setActiveCategory("All")} + sx={{ + height: 26, + borderRadius: 2, + fontSize: "0.7rem", + ...(activeCategory === "All" && { + bgcolor: "primary.dark", + color: "common.white", + "&:hover": { + bgcolor: "secondary.main", + color: "common.white", + }, + }), + }} + /> + {categories.map(category => ( + + + {category} + + } + clickable + variant="filled" + onClick={() => setActiveCategory(category)} + sx={{ + height: 26, + borderRadius: 2, + fontSize: "0.7rem", + ...(activeCategory === category && { + bgcolor: "primary.dark", + color: "common.white", + "&:hover": { + bgcolor: "secondary.main", + color: "common.white", + }, + }), + }} + /> + ))} + + ); +} + +export default CategoryFilterChips; diff --git a/packages/ui/src/components/Summary/SummaryRenderer.tsx b/packages/ui/src/components/Summary/SummaryRenderer.tsx index b62a69a63..75819185d 100644 --- a/packages/ui/src/components/Summary/SummaryRenderer.tsx +++ b/packages/ui/src/components/Summary/SummaryRenderer.tsx @@ -1,10 +1,9 @@ import { Box, GridLegacy } from "@mui/material"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import type { Widget } from "sections"; import { v1 } from "uuid"; import usePermissions from "../../hooks/usePermissions"; -import Chip from "../Chip/Chip"; -import { CATEGORIES, CATEGORY_ICONS, primaryCategory } from "./categoryConfig"; +import CategoryFilterChips from "./CategoryFilterChips"; +import { CATEGORIES, primaryCategory } from "./categoryConfig"; import { useSummaryCategory } from "./SummaryCategoryContext"; type SummaryRendererProps = { @@ -15,7 +14,7 @@ type SummaryRendererProps = { function SummaryRenderer({ widgets, useKeys = true, keyPrefix = "summary" }: SummaryRendererProps) { const { isPartnerPreview } = usePermissions(); - const { activeCategory, setActiveCategory } = useSummaryCategory(); + const { activeCategory } = useSummaryCategory(); const visibleWidgets = widgets.filter( widget => !widget.definition.isPrivate || isPartnerPreview @@ -34,56 +33,7 @@ function SummaryRenderer({ widgets, useKeys = true, keyPrefix = "summary" }: Sum return ( - {presentCategories.length > 1 && ( - - setActiveCategory("All")} - sx={{ - height: 26, - borderRadius: 2, - fontSize: "0.7rem", - ...(activeCategory === "All" && { - bgcolor: "primary.dark", - color: "common.white", - "&:hover": { - bgcolor: "secondary.main", - color: "common.white", - }, - }), - }} - /> - {presentCategories.map(category => ( - - - {category} - - } - clickable - variant="filled" - onClick={() => setActiveCategory(category)} - sx={{ - height: 26, - borderRadius: 2, - fontSize: "0.7rem", - ...(activeCategory === category && { - bgcolor: "primary.dark", - color: "common.white", - "&:hover": { - bgcolor: "secondary.main", - color: "common.white", - }, - }), - }} - /> - ))} - - )} + Date: Wed, 29 Jul 2026 19:28:19 +0100 Subject: [PATCH 8/9] refactor: simplify sticky nav category UI to active-chip + clear Swap the full category filter chip row (too busy in a dropdown that already has its own text filter) for a single chip showing the active category with a delete/clear action, only rendered when a category filter is actually applied. --- .../StickyProfileHeader.tsx | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/packages/ui/src/components/StickyProfileHeader/StickyProfileHeader.tsx b/packages/ui/src/components/StickyProfileHeader/StickyProfileHeader.tsx index 1e5e1bf47..f27507f56 100644 --- a/packages/ui/src/components/StickyProfileHeader/StickyProfileHeader.tsx +++ b/packages/ui/src/components/StickyProfileHeader/StickyProfileHeader.tsx @@ -8,8 +8,8 @@ import { useLocation, useNavigate } from "react-router"; import { scroller } from "react-scroll"; import usePlatformApi from "../../hooks/usePlatformApi"; import CategoryAvatar from "../CategoryAvatar"; -import CategoryFilterChips from "../Summary/CategoryFilterChips"; -import { CATEGORIES, primaryCategory, type Category } from "../Summary/categoryConfig"; +import Chip from "../Chip/Chip"; +import { CATEGORY_ICONS, primaryCategory, type Category } from "../Summary/categoryConfig"; import { useSummaryCategory } from "../Summary/SummaryCategoryContext"; import { PROFILE_TABS_SENTINEL_ID, @@ -46,7 +46,7 @@ function StickyProfileHeader({ const navigate = useNavigate(); const location = useLocation(); const { data, entity } = usePlatformApi(); - const { activeCategory } = useSummaryCategory(); + const { activeCategory, setActiveCategory } = useSummaryCategory(); const [anchorEl, setAnchorEl] = useState(null); const [filterText, setFilterText] = useState(""); const [isVisible, setIsVisible] = useState(false); @@ -62,13 +62,6 @@ function StickyProfileHeader({ [widgets, activeCategory] ); - // Full (unfiltered) category set so the chip row always shows every - // category available on this page, even while one is active. - const presentCategories = useMemo( - () => CATEGORIES.filter((category) => widgets.some((widget) => primaryCategory(widget.definition) === category)), - [widgets] - ); - const ids = useMemo(() => categoryWidgets.map((widget) => widget.definition.id), [categoryWidgets]); const activeId = useActiveSection(ids); const activeWidget = @@ -175,7 +168,28 @@ function StickyProfileHeader({ onKeyDown={(event) => event.stopPropagation()} sx={{ px: 1.5, pb: 1, width: "100%" }} /> - + {activeCategory !== "All" && ( + + + + {activeCategory} + + } + variant="filled" + onDelete={() => setActiveCategory("All")} + sx={{ + height: 26, + borderRadius: 2, + fontSize: "0.7rem", + bgcolor: "primary.dark", + color: "common.white", + "& .MuiChip-deleteIcon": { color: "common.white" }, + }} + /> + + )} {filteredWidgets.map((widget) => { const hasData = entityData ? !!widget.definition.hasData(entityData) : true; return ( From 134d05af70fa6e2f4f7d838fc8276fd1107eea24 Mon Sep 17 00:00:00 2001 From: Carlos Cruz Date: Thu, 30 Jul 2026 00:23:03 +0100 Subject: [PATCH 9/9] fix: keep active-category chip's delete icon visible on hover MUI's default filled-Chip delete-icon hover color darkens (tuned for light chips), which nearly vanished against our primary.dark background. Override to a lighter grey on hover instead. --- .../src/components/StickyProfileHeader/StickyProfileHeader.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ui/src/components/StickyProfileHeader/StickyProfileHeader.tsx b/packages/ui/src/components/StickyProfileHeader/StickyProfileHeader.tsx index f27507f56..6ec5fd23c 100644 --- a/packages/ui/src/components/StickyProfileHeader/StickyProfileHeader.tsx +++ b/packages/ui/src/components/StickyProfileHeader/StickyProfileHeader.tsx @@ -186,6 +186,7 @@ function StickyProfileHeader({ bgcolor: "primary.dark", color: "common.white", "& .MuiChip-deleteIcon": { color: "common.white" }, + "& .MuiChip-deleteIcon:hover": { color: "grey.300" }, }} />