diff --git a/apps/cde-visualization-wc/project.json b/apps/cde-visualization-wc/project.json index b90f089ba..f00e0dbf5 100644 --- a/apps/cde-visualization-wc/project.json +++ b/apps/cde-visualization-wc/project.json @@ -35,7 +35,7 @@ "maximumError": "10kb" } ], - "outputHashing": "all" + "outputHashing": "none" }, "development": { "optimization": false, diff --git a/apps/ftu-ui-small-wc/src/app/app.component.spec.ts b/apps/ftu-ui-small-wc/src/app/app.component.spec.ts index 4f6d9860b..764dacdfa 100644 --- a/apps/ftu-ui-small-wc/src/app/app.component.spec.ts +++ b/apps/ftu-ui-small-wc/src/app/app.component.spec.ts @@ -1,7 +1,13 @@ import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform-browser/animations'; import { Router } from '@angular/router'; import { dispatch, dispatch$, dispatchAction$, select$, selectSnapshot } from '@hra-ui/cdk/injectors'; -import { FTU_DATA_IMPL_ENDPOINTS, FtuDataImplEndpoints, IllustrationMappingItem } from '@hra-ui/services'; +import { + FTU_DATA_IMPL_ENDPOINTS, + FtuDataImplEndpoints, + IllustrationMappingItem, + Iri, + RawIllustration, +} from '@hra-ui/services'; import { ActiveFtuActions, IllustratorActions, IllustratorSelectors } from '@hra-ui/state'; import { ActionContext, ActionStatus, Actions } from '@ngxs/store'; import { ReplaySubject, firstValueFrom, from, of, take, toArray } from 'rxjs'; @@ -48,8 +54,16 @@ describe('AppComponent', () => { }); describe('.selectedIllustration', () => { - const iri = 'foo/bar'; - const illustration = { '@id': iri }; + const iri = 'https://foo.bar/'; + const illustration: RawIllustration = { + '@id': iri as Iri, + label: '', + organ_id: '', + organ_label: '', + representation_of: '', + mapping: [], + illustration_files: [], + }; const params = { queryParams: { id: iri }, }; diff --git a/apps/ftu-ui-small-wc/src/app/app.component.ts b/apps/ftu-ui-small-wc/src/app/app.component.ts index de64e66f7..2bcaa6fa8 100644 --- a/apps/ftu-ui-small-wc/src/app/app.component.ts +++ b/apps/ftu-ui-small-wc/src/app/app.component.ts @@ -23,10 +23,14 @@ import { FullscreenContainerComponent, FullscreenContentComponent } from '@hra-u import { FTU_DATA_IMPL_ENDPOINTS, FtuDataImplEndpoints, + illustrationsInput, + rawCellSummariesInput, RawCellSummary, RawDatasets, + rawDatasetsInput, RawIllustration, RawIllustrationsJsonld, + selectedIllustrationInput, setUrl, } from '@hra-ui/services'; import { @@ -98,13 +102,17 @@ function filterUndefined(): OperatorFunction { export class AppComponent implements OnInit, OnChanges { /** Illustration to display (choosen automatically if not provided) */ @Input() selectedIllustration?: string | RawIllustration; + /** Set of all illustrations */ @Input() illustrations: string | RawIllustrationsJsonld = 'https://cdn.humanatlas.io/digital-objects/graph/2d-ftu-illustrations/latest/assets/2d-ftu-illustrations.jsonld'; + /** Cell summaries to display in tables */ @Input() summaries: string | RawCellSummary = ''; + /** Datasets to display in the sources tab */ @Input() datasets: string | RawDatasets = ''; + /** Base href if different from the page */ @Input() baseHref = ''; @@ -197,9 +205,9 @@ export class AppComponent implements OnInit, OnChanges { if (!endpointsUpdated) { const { illustrations, datasets, summaries, baseHref } = this; this.endpoints.next({ - illustrations, - datasets, - summaries, + illustrations: illustrationsInput(illustrations) ?? '', + datasets: rawDatasetsInput(datasets) ?? '', + summaries: rawCellSummariesInput(summaries) ?? '', baseHref, }); endpointsUpdated = true; @@ -237,7 +245,10 @@ export class AppComponent implements OnInit, OnChanges { * Updates the selected illustration using a default if not provided */ private updateSelectedIllustration(): void { - const { selectedIllustration: selected } = this; + const { selectedIllustration } = this; + console.log('fooo', selectedIllustration); + const selected = selectedIllustrationInput(selectedIllustration); + console.log('bar'); if (selected === undefined || selected === '') { this.setDefaultSelectedIllustration(); } else { diff --git a/apps/ftu-ui/src/app/app.component.spec.ts b/apps/ftu-ui/src/app/app.component.spec.ts index 7ae963b33..2d3423e7e 100644 --- a/apps/ftu-ui/src/app/app.component.spec.ts +++ b/apps/ftu-ui/src/app/app.component.spec.ts @@ -12,7 +12,7 @@ import { selectSnapshot, } from '@hra-ui/cdk/injectors'; import { LinkRegistryActions } from '@hra-ui/cdk/state'; -import { FTU_DATA_IMPL_ENDPOINTS, IllustrationMappingItem } from '@hra-ui/services'; +import { FTU_DATA_IMPL_ENDPOINTS, IllustrationMappingItem, Iri, RawIllustration } from '@hra-ui/services'; import { ActiveFtuActions, IllustratorActions, @@ -71,8 +71,16 @@ describe('AppComponent', () => { }); describe('.selectedIllustration', () => { - const iri = 'foo/bar'; - const illustration = { '@id': iri }; + const iri = 'https://foo.bar/'; + const illustration: RawIllustration = { + '@id': iri as Iri, + label: '', + organ_id: '', + organ_label: '', + representation_of: '', + mapping: [], + illustration_files: [], + }; it('accepts an iri string', async () => { await shallow.render({ bind: { selectedIllustration: iri } }); diff --git a/apps/ftu-ui/src/app/app.component.ts b/apps/ftu-ui/src/app/app.component.ts index 26580c208..b8049c980 100644 --- a/apps/ftu-ui/src/app/app.component.ts +++ b/apps/ftu-ui/src/app/app.component.ts @@ -29,10 +29,14 @@ import { ScreenNoticeBehaviorComponent } from '@hra-ui/components/behavioral'; import { FTU_DATA_IMPL_ENDPOINTS, FtuDataImplEndpoints, + illustrationsInput, + rawCellSummariesInput, RawCellSummary, RawDatasets, + rawDatasetsInput, RawIllustration, RawIllustrationsJsonld, + selectedIllustrationInput, setUrl, } from '@hra-ui/services'; import { @@ -97,13 +101,17 @@ export class AppComponent implements AfterContentInit, OnChanges, OnInit { /** Illustration to display (choosen automatically if not provided) */ @Input() selectedIllustration?: string | RawIllustration; + /** Set of all illustrations */ @Input() illustrations: string | RawIllustrationsJsonld = 'https://cdn.humanatlas.io/digital-objects/graph/2d-ftu-illustrations/latest/assets/2d-ftu-illustrations.jsonld'; + /** Cell summaries to display in tables */ @Input() summaries: string | RawCellSummary = ''; + /** Datasets to display in the sources tab */ @Input() datasets: string | RawDatasets = ''; + /** Base href if different from the page */ @Input() baseHref = ''; @@ -203,9 +211,9 @@ export class AppComponent implements AfterContentInit, OnChanges, OnInit { if (!endpointsUpdated) { const { illustrations, datasets, summaries, baseHref } = this; this.endpoints.next({ - illustrations, - datasets, - summaries, + illustrations: illustrationsInput(illustrations) ?? '', + datasets: rawDatasetsInput(datasets) ?? '', + summaries: rawCellSummariesInput(summaries) ?? '', baseHref, }); endpointsUpdated = true; @@ -248,7 +256,8 @@ export class AppComponent implements AfterContentInit, OnChanges, OnInit { * Updates the selected illustration using a default if not provided */ private updateSelectedIllustration(): void { - const { selectedIllustration: selected } = this; + const { selectedIllustration } = this; + const selected = selectedIllustrationInput(selectedIllustration); if (selected) { const iri = typeof selected === 'string' ? selected : selected['@id']; this.updateLink(LinkIds.ExploreFTU, { diff --git a/apps/medical-illustration/src/app/medical-illustration.component.ts b/apps/medical-illustration/src/app/medical-illustration.component.ts index bfaeef69c..2076fa00d 100644 --- a/apps/medical-illustration/src/app/medical-illustration.component.ts +++ b/apps/medical-illustration/src/app/medical-illustration.component.ts @@ -17,12 +17,15 @@ import { FtuDataImplEndpoints, FtuDataImplService, IllustrationMappingItem, + illustrationsInput, Iri, RAW_ILLUSTRATION, RawCellEntry, RawIllustration, RawIllustrationFile, RawIllustrationsJsonld, + selectedIllustrationInput, + tryParseJson, } from '@hra-ui/services'; import { Observable, of, OperatorFunction, ReplaySubject, switchMap } from 'rxjs'; import { z } from 'zod'; @@ -68,7 +71,7 @@ export class MedicalIllustrationComponent implements OnInit, OnChanges { @Input() selectedIllustration?: string | RawIllustration; /** Optional set of all illustrations. Used when selectedIllustration is an iri */ - @Input() illustrations: string | RawIllustrationsJsonld = ''; + @Input() illustrations?: string | RawIllustrationsJsonld; /** A cell or id to highlight in the illustration */ @Input() highlight?: string | RawCellEntry; @@ -85,11 +88,12 @@ export class MedicalIllustrationComponent implements OnInit, OnChanges { /** Get the normalized id for the highlight input */ get highlightId(): string | undefined { const { highlight } = this; - if (typeof highlight === 'object') { - return highlight.representation_of; + const parsed = tryParseJson(highlight); + if (typeof parsed === 'object') { + return parsed.representation_of; } - return highlight; + return parsed; } /** Data endpoints */ @@ -141,14 +145,14 @@ export class MedicalIllustrationComponent implements OnInit, OnChanges { const { baseHref, illustrations } = this; this.endpoints.next({ baseHref, - illustrations, + illustrations: illustrationsInput(illustrations) ?? '', datasets: '', summaries: '', }); } if ('selectedIllustration' in changes) { - this.illustration$.next(this.selectedIllustration); + this.illustration$.next(selectedIllustrationInput(this.selectedIllustration)); } this.initialized = true; diff --git a/libs/cde-visualization/src/lib/services/data/data-loader.service.ts b/libs/cde-visualization/src/lib/services/data/data-loader.service.ts index a2cf5fae1..d57b8dcfa 100644 --- a/libs/cde-visualization/src/lib/services/data/data-loader.service.ts +++ b/libs/cde-visualization/src/lib/services/data/data-loader.service.ts @@ -29,6 +29,14 @@ export class DataLoaderService { return runInInjectionContext(injector, () => { const loader = inject>(loaderToken); const load = (sourceValue: string | T | undefined): Observable => { + try { + if (typeof sourceValue === 'string') { + sourceValue = JSON.parse(sourceValue); + } + } catch { + // Ignore errors + } + if (typeof sourceValue !== 'string') { return of(sourceValue ?? initialValue); } diff --git a/libs/services/src/index.ts b/libs/services/src/index.ts index 5d50f077f..afb329344 100644 --- a/libs/services/src/index.ts +++ b/libs/services/src/index.ts @@ -10,3 +10,4 @@ export * from './lib/service.module'; export * from './lib/shared/common.model'; export * as FtuDataSchemas from './lib/ftu-data/ftu-data.model'; +export * from './lib/ftu-data/ftu-data.transformers'; diff --git a/libs/services/src/lib/ftu-data/ftu-data.transformers.ts b/libs/services/src/lib/ftu-data/ftu-data.transformers.ts new file mode 100644 index 000000000..a859b8150 --- /dev/null +++ b/libs/services/src/lib/ftu-data/ftu-data.transformers.ts @@ -0,0 +1,128 @@ +import { z } from 'zod'; +import { RAW_CELL_SUMMARIES, RAW_DATASETS, RAW_ILLUSTRATION, RAW_ILLUSTRATIONS_JSONLD } from './ftu-data.model'; + +/** + * Tries to parse a value as json. Returns the original value if it could not be parsed. + * + * @param value Value to parse + * @returns Parsed json value or the original value + */ +export function tryParseJson(value: unknown): R { + try { + if (typeof value === 'string') { + return JSON.parse(value); + } + } catch { + // Ignore errors + } + + return value as R; +} + +/** + * Tests whether a value is path like + * + * @param value Value to test + * @returns True if the value is path like + */ +function isPath(value: unknown): boolean { + try { + if (typeof value === 'string') { + new URL(value, 'https://base.url/'); + return true; + } + } catch { + // Ignore errors + } + + return false; +} + +/** + * Creates a zod schema for validating input values that accepts either an url, + * a path, javascript object, or a json encoded string of such an object. + * + * @param schema Input object schema + * @returns A zod type for validating input values + */ +function createInputValidation(schema: T) { + return z.preprocess( + tryParseJson, + z.union([ + z.string().url().optional(), + z.literal(''), + z.custom((value) => (isPath(value) ? value : false), 'Invalid path'), + schema, + ]), + ); +} + +/** + * Parses a value using the provided schema. Throws an error on parsing failure. + * + * @param schema Schema to use for parsing + * @param value Value to parse + * @returns The parsed value + */ +function parseInput(schema: T, value: unknown): z.infer { + const result = schema.safeParse(value); + if (result.success) { + return result.data; + } + + throw new TypeError( + `Invalid input. Expected an url, a path, or a json encoded ${schema.description} object. Received: ${value}`, + ); +} + +/** Selected illustration input schema */ +export const SELECTED_ILLUSTRATION_INPUT = createInputValidation(RAW_ILLUSTRATION).describe('illustration'); + +/** + * Parses selected illustration input + * + * @param value Value to parse + * @returns Parsed input + */ +export function selectedIllustrationInput(value: unknown) { + return parseInput(SELECTED_ILLUSTRATION_INPUT, value); +} + +/** Illustrations input schema */ +export const ILLUSTRATIONS_INPUT = createInputValidation(RAW_ILLUSTRATIONS_JSONLD).describe('illustrations'); + +/** + * Parses illustrations input + * + * @param value Value to parse + * @returns Parsed input + */ +export function illustrationsInput(value: unknown) { + return parseInput(ILLUSTRATIONS_INPUT, value); +} + +/** Cell summaries input schema */ +export const RAW_CELL_SUMMARIES_INPUT = createInputValidation(RAW_CELL_SUMMARIES).describe('cell summaries'); + +/** + * Parses cell summaries input + * + * @param value Value to parse + * @returns Parsed input + */ +export function rawCellSummariesInput(value: unknown) { + return parseInput(RAW_CELL_SUMMARIES_INPUT, value); +} + +/** Datasets input schema */ +export const RAW_DATASETS_INPUT = createInputValidation(RAW_DATASETS).describe('datasets'); + +/** + * Parses datasets input + * + * @param value Value to parse + * @returns Parsed input + */ +export function rawDatasetsInput(value: unknown) { + return parseInput(RAW_DATASETS_INPUT, value); +} diff --git a/tsconfig.base.json b/tsconfig.base.json index e5a8350cc..7fc9f905c 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -68,8 +68,8 @@ "@hra-ui/design-system/app-logos": [ "libs/design-system/app-logos/src/index.ts" ], - "@hra-ui/design-system/apps-sidenav": [ - "libs/design-system/apps-sidenav/src/index.ts" + "@hra-ui/design-system/apps-card": [ + "libs/design-system/apps-card/src/index.ts" ], "@hra-ui/design-system/brand-logo": [ "libs/design-system/brand-logo/src/index.ts"