From c23b1394b1bf31f67e0b8eb4426bd974ca7958d6 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Wed, 13 Nov 2024 15:00:27 -0500 Subject: [PATCH 1/6] refact: use i18n for data type / entity labels --- .../Beacon/BeaconCommon/BeaconQueryFormUi.tsx | 2 +- src/js/components/Overview/Counts.tsx | 16 +++++++++------- src/js/components/Overview/LastIngestion.tsx | 5 +++-- src/js/components/Search/SearchResultsCounts.tsx | 6 +++--- src/js/components/Search/SearchResultsPane.tsx | 10 +++++++--- .../locales/en/default_translation_en.json | 15 ++++++++++----- .../locales/fr/default_translation_fr.json | 11 ++++++++++- 7 files changed, 43 insertions(+), 22 deletions(-) diff --git a/src/js/components/Beacon/BeaconCommon/BeaconQueryFormUi.tsx b/src/js/components/Beacon/BeaconCommon/BeaconQueryFormUi.tsx index 5c15d909..7dd3c2e3 100644 --- a/src/js/components/Beacon/BeaconCommon/BeaconQueryFormUi.tsx +++ b/src/js/components/Beacon/BeaconCommon/BeaconQueryFormUi.tsx @@ -233,7 +233,7 @@ const BeaconQueryFormUi = ({ {hasVariants && ( { // Break down help into multiple sentences inside an array to make translation a bit easier. const data = [ { - title: 'Individuals', + entity: 'individual', help: ['individual_help_1'], icon: , count: counts.individuals, }, { - title: 'Biosamples', + entity: 'biosample', help: ['biosample_help_1'], icon: , count: counts.biosamples, }, { - title: 'Experiments', + entity: 'experiment', help: ['experiment_help_1', 'experiment_help_2'], icon: , count: counts.experiments, @@ -48,17 +48,19 @@ const Counts = () => { <> {t('Counts')} - {data.map(({ title, help, icon, count }, i) => { - const titleTransl = t(`entities.${title}`); + {data.map(({ entity, help, icon, count }, i) => { + // false count – just need the highest form of plural + // - see https://www.i18next.com/translation-function/plurals + const title = t(`entities.${entity}`, { count: 100 }); return ( - {titleTransl} + {title} {help && ( {help.map((h, i) => ( diff --git a/src/js/components/Overview/LastIngestion.tsx b/src/js/components/Overview/LastIngestion.tsx index f00b19c0..f21bd043 100644 --- a/src/js/components/Overview/LastIngestion.tsx +++ b/src/js/components/Overview/LastIngestion.tsx @@ -5,7 +5,6 @@ import { CalendarOutlined } from '@ant-design/icons'; import { useTranslation } from 'react-i18next'; import { useAppSelector } from '@/hooks'; -import { getDataTypeLabel } from '@/types/dataTypes'; import type { LastIngestionDataTypeResponse } from '@/types/lastIngestionDataTypeResponse'; import { BOX_SHADOW } from '@/constants/overviewConstants'; @@ -48,7 +47,9 @@ const LastIngestionInfo: React.FC = () => { - {t(getDataTypeLabel(dataType.id))} + {/* false count – just need the highest form of plural + - see https://www.i18next.com/translation-function/plurals */} + {t(`entities.${dataType.id}`, { count: 100 })} {' '} diff --git a/src/js/components/Search/SearchResultsCounts.tsx b/src/js/components/Search/SearchResultsCounts.tsx index 9d778aad..559599b2 100644 --- a/src/js/components/Search/SearchResultsCounts.tsx +++ b/src/js/components/Search/SearchResultsCounts.tsx @@ -58,7 +58,7 @@ const SearchResultsCounts = ({ ].join(' ')} > } /> } diff --git a/src/js/components/Search/SearchResultsPane.tsx b/src/js/components/Search/SearchResultsPane.tsx index d1f971fc..2b804cb7 100644 --- a/src/js/components/Search/SearchResultsPane.tsx +++ b/src/js/components/Search/SearchResultsPane.tsx @@ -33,7 +33,7 @@ const SearchResultsPane = ({ () => [ { dataIndex: 'id', - title: t('entities.Individual'), + title: t('entities.individual', { count: 1 }), render: (id: string) => ( {id} @@ -85,7 +85,9 @@ const SearchResultsPane = ({ <> - {t('entities.Biosamples')} + {/* false count – just need the highest form of plural + - see https://www.i18next.com/translation-function/plurals */} + {t('entities.biosample', { count: 100 })} {!hasInsufficientData && biosampleChartData.length ? ( @@ -95,7 +97,9 @@ const SearchResultsPane = ({ - {t('entities.Experiments')} + {/* false count – just need the highest form of plural + - see https://www.i18next.com/translation-function/plurals */} + {t('entities.experiment', { count: 100 })} {!hasInsufficientData && experimentChartData.length ? ( diff --git a/src/public/locales/en/default_translation_en.json b/src/public/locales/en/default_translation_en.json index 92a1f624..2f892ab6 100644 --- a/src/public/locales/en/default_translation_en.json +++ b/src/public/locales/en/default_translation_en.json @@ -6,16 +6,21 @@ "Portal": "Portal", "missing": "missing", "entities": { - "Phenopackets": "Clinical Data", + "phenopacket_one": "Phenopacket", + "phenopacket_other": "Clinical Data", + "individual_one": "Individual", + "individual_other": "Individuals", "Individual": "Individual", - "Individuals": "Individuals", "individual_help_1": "Individuals represent a specific person / organism, and may have one or multiple associated biosamples, each with associated experiments.", - "Biosamples": "Biosamples", + "biosample_one": "Biosample", + "biosample_other": "Biosamples", "biosample_help_1": "Biosamples are usually biological material extracted from a specific individual.", - "Experiments": "Experiments", + "experiment_one": "Experiment", + "experiment_other": "Experiments", "experiment_help_1": "Experiments are a process done to a specific sample, e.g., whole-genome sequencing.", "experiment_help_2": "One lab experiment may result in multiple experiment records inside the portal, such as with multiplexed samples.", - "Variants": "Variants" + "variant_one": "Variant", + "variant_other": "Variants" }, "in": "in", "Get Data": "Get Data", diff --git a/src/public/locales/fr/default_translation_fr.json b/src/public/locales/fr/default_translation_fr.json index 3a78a7cf..1e0794f6 100644 --- a/src/public/locales/fr/default_translation_fr.json +++ b/src/public/locales/fr/default_translation_fr.json @@ -6,16 +6,25 @@ "Portal": "Portail", "missing": "manquant(s)", "entities": { + "phenopacket_one": "Phenopacket", + "phenopacket_other": "Données cliniques", "Phenopackets": "Données cliniques", + "individual_one": "Participant", + "individual_other": "Participants", "Individual": "Participant", "Individuals": "Participants", "individual_help_1": "Les participants représentent une personne/un organisme spécifique et peuvent avoir un ou plusieurs échantillons biologiques associés, chacun avec des expériences associées.", + "biosample_one": "Échantillon biologique", + "biosample_other": "Échantillons biologiques", "Biosamples": "Échantillons biologiques", "biosample_help_1": "Les échantillons biologiques sont généralement du matériel biologique extrait d’un participant spécifique.", + "experiment_one": "Expérience", + "experiment_other": "Expériences", "Experiments": "Expériences", "experiment_help_1": "Les expériences sont un processus effectué sur un échantillon spécifique, par exemple le séquençage du génome entier.", "experiment_help_2": "Une expérience par un laboratoire peut donner lieu à plusieurs enregistrements d'expériences dans le portail, par exemple avec des échantillons multiplexés.", - "Variants": "Variants" + "variant_one": "Variant", + "variant_other": "Variants" }, "in": "en", "Get Data": "Obtenir", From 688c573c873b9190cf45218d4de98259a413b9c7 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Wed, 13 Nov 2024 15:06:10 -0500 Subject: [PATCH 2/6] chore: rm unused data type stuff --- src/js/types/dataTypes.ts | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/src/js/types/dataTypes.ts b/src/js/types/dataTypes.ts index 07fbd03d..e69de29b 100644 --- a/src/js/types/dataTypes.ts +++ b/src/js/types/dataTypes.ts @@ -1,22 +0,0 @@ -export enum DataTypes { - phenopacket = 'phenopacket', - experiment = 'experiment', - experiment_result = 'experiment_result', - variant = 'variant', -} - -export const DataTypesLabels = { - [DataTypes.phenopacket]: 'entities.Phenopackets', - [DataTypes.experiment]: 'entities.Experiments', - [DataTypes.experiment_result]: 'entities.Experiment Results', - [DataTypes.variant]: 'entities.Variants', -}; - -export const getDataTypeLabel = (dataTypeString: string): string => { - const dataTypesValues = Object.values(DataTypes) as string[]; - if (dataTypesValues.includes(dataTypeString)) { - const dataType: DataTypes = DataTypes[dataTypeString as keyof typeof DataTypes]; - return DataTypesLabels[dataType]; - } - return 'Unknown Data Type'; -}; From 292081aad31de00b5943f3bec8ced78d6c501d86 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Wed, 13 Nov 2024 15:07:13 -0500 Subject: [PATCH 3/6] chore: clean up default french transl --- src/public/locales/fr/default_translation_fr.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/public/locales/fr/default_translation_fr.json b/src/public/locales/fr/default_translation_fr.json index 1e0794f6..340f26ab 100644 --- a/src/public/locales/fr/default_translation_fr.json +++ b/src/public/locales/fr/default_translation_fr.json @@ -8,19 +8,14 @@ "entities": { "phenopacket_one": "Phenopacket", "phenopacket_other": "Données cliniques", - "Phenopackets": "Données cliniques", "individual_one": "Participant", "individual_other": "Participants", - "Individual": "Participant", - "Individuals": "Participants", "individual_help_1": "Les participants représentent une personne/un organisme spécifique et peuvent avoir un ou plusieurs échantillons biologiques associés, chacun avec des expériences associées.", "biosample_one": "Échantillon biologique", "biosample_other": "Échantillons biologiques", - "Biosamples": "Échantillons biologiques", "biosample_help_1": "Les échantillons biologiques sont généralement du matériel biologique extrait d’un participant spécifique.", "experiment_one": "Expérience", "experiment_other": "Expériences", - "Experiments": "Expériences", "experiment_help_1": "Les expériences sont un processus effectué sur un échantillon spécifique, par exemple le séquençage du génome entier.", "experiment_help_2": "Une expérience par un laboratoire peut donner lieu à plusieurs enregistrements d'expériences dans le portail, par exemple avec des échantillons multiplexés.", "variant_one": "Variant", From ade4daceb9326317b2c3f33cb0cf505eee5ba178 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Wed, 13 Nov 2024 15:27:22 -0500 Subject: [PATCH 4/6] refact: rewrite data types reducer + fetch even if not auth'd --- package-lock.json | 1 + package.json | 1 + src/js/components/BentoAppRouter.tsx | 6 +-- src/js/features/config/config.store.ts | 6 ++- src/js/features/dataTypes/dataTypes.store.ts | 49 ++++++++++++-------- src/js/store.ts | 2 + src/js/types/dataTypes.ts | 12 +++++ 7 files changed, 53 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index b76eb83c..394d64de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "redux-thunk": "^2.4.1" }, "devDependencies": { + "@types/json-schema": "^7.0.15", "@types/react-dom": "^18.3.1", "@typescript-eslint/eslint-plugin": "^7.16.1", "@typescript-eslint/parser": "^7.16.1", diff --git a/package.json b/package.json index 5f4b2b63..22def57c 100755 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "redux-thunk": "^2.4.1" }, "devDependencies": { + "@types/json-schema": "^7.0.15", "@types/react-dom": "^18.3.1", "@typescript-eslint/eslint-plugin": "^7.16.1", "@typescript-eslint/parser": "^7.16.1", diff --git a/src/js/components/BentoAppRouter.tsx b/src/js/components/BentoAppRouter.tsx index 38644cbb..c68f8278 100644 --- a/src/js/components/BentoAppRouter.tsx +++ b/src/js/components/BentoAppRouter.tsx @@ -106,10 +106,8 @@ const BentoAppRouter = () => { dispatch(makeGetAboutRequest()); dispatch(fetchGohanData()); dispatch(makeGetServiceInfoRequest()); - if (isAuthenticated) { - dispatch(makeGetDataTypes()); - } - }, [dispatch, isAuthenticated]); + dispatch(makeGetDataTypes()); + }, [dispatch]); if (isAutoAuthenticating || projectsStatus === RequestStatus.Pending) { return ; diff --git a/src/js/features/config/config.store.ts b/src/js/features/config/config.store.ts index 4be6d1ac..13f89502 100644 --- a/src/js/features/config/config.store.ts +++ b/src/js/features/config/config.store.ts @@ -52,7 +52,11 @@ export const makeGetServiceInfoRequest = createAsyncThunk< condition(_, { getState }) { const { serviceInfoStatus } = getState().config; const cond = serviceInfoStatus === RequestStatus.Idle; - if (!cond) console.debug(`makeGetServiceInfoRequest(), but a prior attempt gave status: ${serviceInfoStatus}`); + if (!cond) { + console.debug( + `makeGetServiceInfoRequest() was attempted, but a prior attempt gave status: ${serviceInfoStatus}` + ); + } return cond; }, } diff --git a/src/js/features/dataTypes/dataTypes.store.ts b/src/js/features/dataTypes/dataTypes.store.ts index a57f59ba..e148af8e 100644 --- a/src/js/features/dataTypes/dataTypes.store.ts +++ b/src/js/features/dataTypes/dataTypes.store.ts @@ -1,34 +1,45 @@ import axios from 'axios'; -import type { PayloadAction } from '@reduxjs/toolkit'; import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import type { RootState } from '@/store'; +import type { BentoServiceDataType } from '@/types/dataTypes'; +import { RequestStatus } from '@/types/requests'; import { authorizedRequestConfig } from '@/utils/requests'; -// TODO: find a way to allow this without an auth token export const makeGetDataTypes = createAsyncThunk< - object, + BentoServiceDataType[], void, { rejectValue: string; state: RootState; } ->('dataTypes/makeGetDataTypes', async (_, { getState }) => { - // Not scoped currently - this is a way to get all data types in an instance from service registry, but it may make - // sense in the future to forward query params to nested calls depending on scope value especially in the case of - // count handling. TBD - TODO: figure this out - const res = await axios.get('/api/service-registry/data-types', authorizedRequestConfig(getState())); - return res.data; -}); +>( + 'dataTypes/makeGetDataTypes', + async (_, { getState }) => { + // Not scoped currently - this is a way to get all data types in an instance from service registry, but it may make + // sense in the future to forward query params to nested calls depending on scope value especially in the case of + // count handling. TBD - TODO: figure this out + const res = await axios.get('/api/service-registry/data-types', authorizedRequestConfig(getState())); + return res.data; + }, + { + condition(_, { getState }) { + const { status } = getState().dataTypes; + const cond = status === RequestStatus.Idle; + if (!cond) console.debug(`makeGetDataTypes() was attempted, but a prior attempt gave status: ${status}`); + return cond; + }, + } +); export type DataTypesState = { - isFetching: boolean; - dataTypes: object; + status: RequestStatus; + dataTypesById: Record; }; const initialState: DataTypesState = { - isFetching: false, - dataTypes: {}, + status: RequestStatus.Idle, + dataTypesById: {}, }; const dataTypes = createSlice({ @@ -37,14 +48,14 @@ const dataTypes = createSlice({ reducers: {}, extraReducers(builder) { builder.addCase(makeGetDataTypes.pending, (state) => { - state.isFetching = true; + state.status = RequestStatus.Pending; }); - builder.addCase(makeGetDataTypes.fulfilled, (state, { payload }: PayloadAction) => { - state.isFetching = false; - state.dataTypes = { ...payload }; + builder.addCase(makeGetDataTypes.fulfilled, (state, { payload }) => { + state.status = RequestStatus.Fulfilled; + state.dataTypesById = Object.fromEntries(payload.map((dt) => [dt.id, dt])); }); builder.addCase(makeGetDataTypes.rejected, (state) => { - state.isFetching = false; + state.status = RequestStatus.Rejected; }); }, }); diff --git a/src/js/store.ts b/src/js/store.ts index b6f954b3..d7bd59cb 100644 --- a/src/js/store.ts +++ b/src/js/store.ts @@ -7,6 +7,7 @@ import { LS_OPENID_CONFIG_KEY, AuthReducer as auth, OIDCReducer as openIdConfigu import configReducer from '@/features/config/config.store'; import contentReducer from '@/features/content/content.store'; import dataReducer from '@/features/data/data.store'; +import dataTypesReducer from '@/features/dataTypes/dataTypes.store'; import queryReducer from '@/features/search/query.store'; import lastIngestionDataReducer from '@/features/ingestion/lastIngestion.store'; import beaconReducer from './features/beacon/beacon.store'; @@ -32,6 +33,7 @@ export const store = configureStore({ config: configReducer, content: contentReducer, data: dataReducer, + dataTypes: dataTypesReducer, query: queryReducer, lastIngestionData: lastIngestionDataReducer, beacon: beaconReducer, diff --git a/src/js/types/dataTypes.ts b/src/js/types/dataTypes.ts index e69de29b..36ef5493 100644 --- a/src/js/types/dataTypes.ts +++ b/src/js/types/dataTypes.ts @@ -0,0 +1,12 @@ +import type { JSONSchema7 } from 'json-schema'; + +export type BentoDataType = { + id: string; + label: string; + queryable: boolean; + schema: JSONSchema7; + metadata_schema: JSONSchema7; + count?: number; +}; + +export type BentoServiceDataType = BentoDataType & { service_base_url: string }; From de6269b79371e61e686bc8aa889ee96d86725c57 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Thu, 14 Nov 2024 12:52:19 -0500 Subject: [PATCH 5/6] chore: clean up stray old en transl --- src/public/locales/en/default_translation_en.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/public/locales/en/default_translation_en.json b/src/public/locales/en/default_translation_en.json index 2f892ab6..21bd29ea 100644 --- a/src/public/locales/en/default_translation_en.json +++ b/src/public/locales/en/default_translation_en.json @@ -10,7 +10,6 @@ "phenopacket_other": "Clinical Data", "individual_one": "Individual", "individual_other": "Individuals", - "Individual": "Individual", "individual_help_1": "Individuals represent a specific person / organism, and may have one or multiple associated biosamples, each with associated experiments.", "biosample_one": "Biosample", "biosample_other": "Biosamples", From e10976b85d56e8fa25e85559a5d1aa013ca9ae52 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Thu, 14 Nov 2024 12:54:14 -0500 Subject: [PATCH 6/6] refact: i18n count constants --- .../Beacon/BeaconCommon/BeaconQueryFormUi.tsx | 3 ++- src/js/components/Overview/Counts.tsx | 5 ++--- src/js/components/Overview/LastIngestion.tsx | 5 ++--- src/js/components/Search/SearchResultsCounts.tsx | 7 ++++--- src/js/components/Search/SearchResultsPane.tsx | 11 ++++------- src/js/constants/i18n.ts | 4 ++++ 6 files changed, 18 insertions(+), 17 deletions(-) create mode 100644 src/js/constants/i18n.ts diff --git a/src/js/components/Beacon/BeaconCommon/BeaconQueryFormUi.tsx b/src/js/components/Beacon/BeaconCommon/BeaconQueryFormUi.tsx index 7dd3c2e3..1452b66a 100644 --- a/src/js/components/Beacon/BeaconCommon/BeaconQueryFormUi.tsx +++ b/src/js/components/Beacon/BeaconCommon/BeaconQueryFormUi.tsx @@ -26,6 +26,7 @@ import { BUTTON_STYLE, CARD_STYLES, } from '@/constants/beaconConstants'; +import { T_PLURAL_COUNT } from '@/constants/i18n'; const STARTER_FILTER = { index: 1, active: true }; const VARIANTS_FORM_ERROR_MESSAGE = @@ -233,7 +234,7 @@ const BeaconQueryFormUi = ({ {hasVariants && ( { {t('Counts')} {data.map(({ entity, help, icon, count }, i) => { - // false count – just need the highest form of plural - // - see https://www.i18next.com/translation-function/plurals - const title = t(`entities.${entity}`, { count: 100 }); + const title = t(`entities.${entity}`, T_PLURAL_COUNT); return ( { @@ -47,9 +48,7 @@ const LastIngestionInfo: React.FC = () => { - {/* false count – just need the highest form of plural - - see https://www.i18next.com/translation-function/plurals */} - {t(`entities.${dataType.id}`, { count: 100 })} + {t(`entities.${dataType.id}`, T_PLURAL_COUNT)} {' '} diff --git a/src/js/components/Search/SearchResultsCounts.tsx b/src/js/components/Search/SearchResultsCounts.tsx index 559599b2..35ff9a0d 100644 --- a/src/js/components/Search/SearchResultsCounts.tsx +++ b/src/js/components/Search/SearchResultsCounts.tsx @@ -3,6 +3,7 @@ import { Skeleton, Space, Statistic } from 'antd'; import { TeamOutlined } from '@ant-design/icons'; import { BiDna } from 'react-icons/bi'; +import { T_PLURAL_COUNT } from '@/constants/i18n'; import { COUNTS_FILL } from '@/constants/overviewConstants'; import { NO_RESULTS_DASHES } from '@/constants/searchConstants'; import ExpSvg from '@/components/Util/ExpSvg'; @@ -58,7 +59,7 @@ const SearchResultsCounts = ({ ].join(' ')} > } /> } diff --git a/src/js/components/Search/SearchResultsPane.tsx b/src/js/components/Search/SearchResultsPane.tsx index 2b804cb7..db003cd6 100644 --- a/src/js/components/Search/SearchResultsPane.tsx +++ b/src/js/components/Search/SearchResultsPane.tsx @@ -4,6 +4,7 @@ import { LeftOutlined } from '@ant-design/icons'; import { PieChart } from 'bento-charts'; import { PORTAL_URL } from '@/config'; +import { T_PLURAL_COUNT, T_SINGULAR_COUNT } from '@/constants/i18n'; import { BOX_SHADOW, PIE_CHART_HEIGHT } from '@/constants/overviewConstants'; import { useTranslationFn } from '@/hooks'; import type { DiscoveryResults } from '@/types/data'; @@ -33,7 +34,7 @@ const SearchResultsPane = ({ () => [ { dataIndex: 'id', - title: t('entities.individual', { count: 1 }), + title: t('entities.individual', T_SINGULAR_COUNT), render: (id: string) => ( {id} @@ -85,9 +86,7 @@ const SearchResultsPane = ({ <> - {/* false count – just need the highest form of plural - - see https://www.i18next.com/translation-function/plurals */} - {t('entities.biosample', { count: 100 })} + {t('entities.biosample', T_PLURAL_COUNT)} {!hasInsufficientData && biosampleChartData.length ? ( @@ -97,9 +96,7 @@ const SearchResultsPane = ({ - {/* false count – just need the highest form of plural - - see https://www.i18next.com/translation-function/plurals */} - {t('entities.experiment', { count: 100 })} + {t('entities.experiment', T_PLURAL_COUNT)} {!hasInsufficientData && experimentChartData.length ? ( diff --git a/src/js/constants/i18n.ts b/src/js/constants/i18n.ts new file mode 100644 index 00000000..f7d27e20 --- /dev/null +++ b/src/js/constants/i18n.ts @@ -0,0 +1,4 @@ +// false count – just need the highest form of plural +// - see https://www.i18next.com/translation-function/plurals +export const T_PLURAL_COUNT = { count: 100 }; +export const T_SINGULAR_COUNT = { count: 1 };