diff --git a/annotation-interface/common/constants.ts b/annotation-interface/common/constants.ts index db89ece99..4d151befb 100644 --- a/annotation-interface/common/constants.ts +++ b/annotation-interface/common/constants.ts @@ -1,5 +1,6 @@ export const supportedLanguages = ['English', 'German'] as const; export type SupportedLanguage = typeof supportedLanguages[number]; +export const pharMeLanguage: SupportedLanguage = 'English'; export const brickUsages = [ 'Drug class', diff --git a/annotation-interface/components/bricks/AutocompleteArea.tsx b/annotation-interface/components/bricks/AutocompleteArea.tsx index 82be4efa8..2eb25a7ff 100644 --- a/annotation-interface/components/bricks/AutocompleteArea.tsx +++ b/annotation-interface/components/bricks/AutocompleteArea.tsx @@ -5,11 +5,14 @@ import AutocompleteMenu, { AutoCompleteMenuRef } from './AutocompleteMenu'; type Props = { value: string; onChange: (text: string) => void; + validPlaceholders: string[]; }; -const validPlaceholders = ['drug-name', 'gene-symbol', 'gene-result']; - -const AutocompleteArea = ({ value: text, onChange: setText }: Props) => { +const AutocompleteArea = ({ + value: text, + onChange: setText, + validPlaceholders, +}: Props) => { const [selection, setSelection] = useState< [start: number, end: number] | null >(null); diff --git a/annotation-interface/components/bricks/BrickForm.tsx b/annotation-interface/components/bricks/BrickForm.tsx index 2e0c446d0..b6f51c56e 100644 --- a/annotation-interface/components/bricks/BrickForm.tsx +++ b/annotation-interface/components/bricks/BrickForm.tsx @@ -10,11 +10,17 @@ import { supportedLanguages, } from '../../common/constants'; import { - ITextBrick, - ITextBrickTranslation, translationIsValid, translationsToArray, translationsToMap, +} from '../../database/helpers/brick-translations'; +import { + allBrickPlaceholders, + medicationBrickPlaceholders, +} from '../../database/helpers/resolve-bricks'; +import { + ITextBrick, + ITextBrickTranslation, } from '../../database/models/TextBrick'; import SelectionPopover from '../common/SelectionPopover'; import WithIcon from '../common/WithIcon'; @@ -120,6 +126,11 @@ const BrickForm = ({ usage, brick }: Props) => { onChange={(text) => updateTranslation(language, text) } + validPlaceholders={ + usage.startsWith('Drug') + ? [...medicationBrickPlaceholders] + : [...allBrickPlaceholders] + } /> ))} diff --git a/annotation-interface/database/helpers/brick-translations.ts b/annotation-interface/database/helpers/brick-translations.ts new file mode 100644 index 000000000..dd2561e06 --- /dev/null +++ b/annotation-interface/database/helpers/brick-translations.ts @@ -0,0 +1,29 @@ +import { SupportedLanguage } from '../../common/constants'; +import { ITextBrickTranslation } from '../models/TextBrick'; +import { OptionalId } from './types'; + +export function translationsToMap( + translations: ITextBrickTranslation[], +): Map { + const map = new Map(); + if (translations) { + translations.forEach((translation) => + map.set(translation.language, translation.text), + ); + } + return map; +} + +export function translationsToArray( + translations: Map, +): ITextBrickTranslation[] { + return Array.from(translations.entries()).map(([language, text]) => { + return { language, text }; + }); +} + +export function translationIsValid( + translation: ITextBrickTranslation, +): boolean { + return translation.text.length > 0; +} diff --git a/annotation-interface/database/connect.ts b/annotation-interface/database/helpers/connect.ts similarity index 100% rename from annotation-interface/database/connect.ts rename to annotation-interface/database/helpers/connect.ts diff --git a/annotation-interface/database/helpers/resolve-bricks.ts b/annotation-interface/database/helpers/resolve-bricks.ts new file mode 100644 index 000000000..3c4cb86ad --- /dev/null +++ b/annotation-interface/database/helpers/resolve-bricks.ts @@ -0,0 +1,85 @@ +import { FilterQuery, Types } from 'mongoose'; + +import { pharMeLanguage, SupportedLanguage } from '../../common/constants'; +import { + ServerGuidelineOverview, + ServerMedication, +} from '../../common/server-types'; +import { IGuidelineAnnotation } from '../models/GuidelineAnnotation'; +import { IMedAnnotation } from '../models/MedAnnotation'; +import TextBrick, { ITextBrick } from '../models/TextBrick'; +import { translationsToMap } from './brick-translations'; +import { MongooseId, OptionalId } from './types'; + +export const medicationBrickPlaceholders = ['drug-name'] as const; +export const allBrickPlaceholders = [ + ...medicationBrickPlaceholders, + 'gene-symbol', + 'gene-result', +] as const; +type BrickPlaceholderValues = { + [Property in typeof allBrickPlaceholders[number]]?: string; +}; + +type BrickResolver = + | { from: 'medAnnotation'; with: IMedAnnotation } + | { from: 'serverMedication'; with: ServerMedication } + | { from: 'guidelineAnnotation'; with: IGuidelineAnnotation } + | { from: 'serverGuideline'; with: ServerGuidelineOverview }; + +const getPlaceholders = ({ + from: type, + with: resolver, +}: BrickResolver): BrickPlaceholderValues => { + switch (type) { + case 'medAnnotation': + return { 'drug-name': resolver.medicationName }; + case 'serverMedication': + return { 'drug-name': resolver.name }; + case 'guidelineAnnotation': + return { + 'drug-name': resolver.medicationName, + 'gene-symbol': resolver.geneSymbol, + 'gene-result': resolver.geneResult, + }; + case 'serverGuideline': + return { + 'drug-name': resolver.medication.name, + 'gene-symbol': resolver.phenotype.geneSymbol.name, + 'gene-result': resolver.phenotype.geneResult.name, + }; + } +}; + +export type ResolvedBrick = [ + _id: IdT, + text: string | undefined, +]; + +export function resolveBricks( + resolver: BrickResolver, + bricks: ITextBrick[], + language: SupportedLanguage = pharMeLanguage, +): ResolvedBrick[] { + const placeholders = getPlaceholders(resolver); + const resolved = bricks.map(({ _id, translations }) => { + let text = translationsToMap(translations).get(language); + if (text) { + Object.entries(placeholders).forEach(([placeholder, replace]) => { + text = text!.replaceAll(`#${placeholder}`, replace); + }); + } + return [_id, text] as ResolvedBrick; + }); + + return resolved; +} + +export const findResolvedBricks = async ( + resolver: BrickResolver, + filter: FilterQuery>, + language: SupportedLanguage = pharMeLanguage, +): Promise[]> => { + const bricks = await TextBrick!.find(filter).lean().exec(); + return resolveBricks(resolver, bricks, language); +}; diff --git a/annotation-interface/database/types.ts b/annotation-interface/database/helpers/types.ts similarity index 100% rename from annotation-interface/database/types.ts rename to annotation-interface/database/helpers/types.ts diff --git a/annotation-interface/database/models/AbstractAnnotation.ts b/annotation-interface/database/models/AbstractAnnotation.ts index 4ba9f6a2b..3f3f1f268 100644 --- a/annotation-interface/database/models/AbstractAnnotation.ts +++ b/annotation-interface/database/models/AbstractAnnotation.ts @@ -1,7 +1,7 @@ import mongoose, { SchemaValidator, Types } from 'mongoose'; import { BrickUsage } from '../../common/constants'; -import { IBaseModel, OptionalId } from '../types'; +import { IBaseModel, OptionalId } from '../helpers/types'; import TextBrick from './TextBrick'; export interface IAbstractAnnotation diff --git a/annotation-interface/database/models/GuidelineAnnotation.ts b/annotation-interface/database/models/GuidelineAnnotation.ts index 13839e228..e701afa2b 100644 --- a/annotation-interface/database/models/GuidelineAnnotation.ts +++ b/annotation-interface/database/models/GuidelineAnnotation.ts @@ -1,6 +1,6 @@ import mongoose, { Types } from 'mongoose'; -import { MongooseId, OptionalId } from '../types'; +import { MongooseId, OptionalId } from '../helpers/types'; import AbstractAnnotation, { annotationBrickValidators, IAbstractAnnotation, diff --git a/annotation-interface/database/models/MedAnnotation.ts b/annotation-interface/database/models/MedAnnotation.ts index d0d76a92b..e8b25f4c2 100644 --- a/annotation-interface/database/models/MedAnnotation.ts +++ b/annotation-interface/database/models/MedAnnotation.ts @@ -1,6 +1,6 @@ import mongoose, { Types } from 'mongoose'; -import { MongooseId, OptionalId } from '../types'; +import { MongooseId, OptionalId } from '../helpers/types'; import AbstractAnnotation, { annotationBrickValidators, IAbstractAnnotation, diff --git a/annotation-interface/database/models/TextBrick.ts b/annotation-interface/database/models/TextBrick.ts index 3d1217eea..1ca25f704 100644 --- a/annotation-interface/database/models/TextBrick.ts +++ b/annotation-interface/database/models/TextBrick.ts @@ -6,7 +6,8 @@ import { BrickUsage, supportedLanguages, } from '../../common/constants'; -import { IBaseModel, OptionalId } from '../types'; +import { translationIsValid } from '../helpers/brick-translations'; +import { IBaseModel, OptionalId } from '../helpers/types'; export interface ITextBrickTranslation extends IBaseModel { @@ -20,32 +21,6 @@ export interface ITextBrick translations: ITextBrickTranslation[]; } -export function translationsToMap( - translations: ITextBrickTranslation[], -): Map { - const map = new Map(); - if (translations) { - translations.forEach((translation) => - map.set(translation.language, translation.text), - ); - } - return map; -} - -export function translationsToArray( - translations: Map, -): ITextBrickTranslation[] { - return Array.from(translations.entries()).map(([language, text]) => { - return { language, text }; - }); -} - -export function translationIsValid( - translation: ITextBrickTranslation, -): boolean { - return translation.text.length > 0; -} - // prevent client side from trying to use node module export default !mongoose.models ? undefined diff --git a/annotation-interface/pages/api/bricks/[id].ts b/annotation-interface/pages/api/bricks/[id].ts index 91673c681..f1fc7cfdb 100644 --- a/annotation-interface/pages/api/bricks/[id].ts +++ b/annotation-interface/pages/api/bricks/[id].ts @@ -1,6 +1,6 @@ import { NextApiHandler } from 'next'; -import dbConnect from '../../../database/connect'; +import dbConnect from '../../../database/helpers/connect'; import TextBrick from '../../../database/models/TextBrick'; const brickApi: NextApiHandler = async (req, res) => { diff --git a/annotation-interface/pages/api/bricks/index.ts b/annotation-interface/pages/api/bricks/index.ts index c8c6ca435..6f585452d 100644 --- a/annotation-interface/pages/api/bricks/index.ts +++ b/annotation-interface/pages/api/bricks/index.ts @@ -1,6 +1,6 @@ import { NextApiHandler } from 'next'; -import dbConnect from '../../../database/connect'; +import dbConnect from '../../../database/helpers/connect'; import TextBrick from '../../../database/models/TextBrick'; const brickApi: NextApiHandler = async (req, res) => { diff --git a/annotation-interface/pages/bricks/[id].tsx b/annotation-interface/pages/bricks/[id].tsx index 9dfd810fb..eb936a3c5 100644 --- a/annotation-interface/pages/bricks/[id].tsx +++ b/annotation-interface/pages/bricks/[id].tsx @@ -16,7 +16,7 @@ import { displayCategoryForIndex, indexForDisplayCategory, } from '../../contexts/brickFilter'; -import dbConnect from '../../database/connect'; +import dbConnect from '../../database/helpers/connect'; import TextBrick, { ITextBrick } from '../../database/models/TextBrick'; const EditBrick = ({ diff --git a/annotation-interface/pages/bricks/index.tsx b/annotation-interface/pages/bricks/index.tsx index 832d16be0..e512421b5 100644 --- a/annotation-interface/pages/bricks/index.tsx +++ b/annotation-interface/pages/bricks/index.tsx @@ -13,7 +13,7 @@ import { useBrickFilterContext, } from '../../contexts/brickFilter'; import { useLanguageContext } from '../../contexts/language'; -import dbConnect from '../../database/connect'; +import dbConnect from '../../database/helpers/connect'; import TextBrick from '../../database/models/TextBrick'; const AllTextBricks = ({