From 17a31ca58789d0998f11a494077ad5ed3c586141 Mon Sep 17 00:00:00 2001 From: Sjur Sutterud Sagen Date: Wed, 29 Apr 2026 13:10:51 +0200 Subject: [PATCH 01/12] Generate newest api-client from spec --- .../pxweb2-api-client/src/models/Dataset.ts | 4 +-- .../src/models/OutputFormatParamType.ts | 2 ++ .../src/models/jsonstat_extension_link.ts | 28 +++++++++++++++++++ .../src/models/jsonstat_link.ts | 4 +++ 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/packages/pxweb2-api-client/src/models/Dataset.ts b/packages/pxweb2-api-client/src/models/Dataset.ts index 5a50b0e12..463e122c3 100644 --- a/packages/pxweb2-api-client/src/models/Dataset.ts +++ b/packages/pxweb2-api-client/src/models/Dataset.ts @@ -6,7 +6,7 @@ import type { ClassType } from './ClassType'; import type { Dimension } from './Dimension'; import type { extension_root } from './extension_root'; import type { href } from './href'; -import type { jsonstat_link } from './jsonstat_link'; +import type { jsonstat_extension_link } from './jsonstat_extension_link'; import type { jsonstat_note } from './jsonstat_note'; import type { label } from './label'; import type { Role } from './Role'; @@ -29,7 +29,7 @@ export type Dataset = { label?: label; source?: source; updated?: updated; - link?: jsonstat_link; + link?: jsonstat_extension_link; /** * Note for table */ diff --git a/packages/pxweb2-api-client/src/models/OutputFormatParamType.ts b/packages/pxweb2-api-client/src/models/OutputFormatParamType.ts index d0b45f635..4af9fe3f1 100644 --- a/packages/pxweb2-api-client/src/models/OutputFormatParamType.ts +++ b/packages/pxweb2-api-client/src/models/OutputFormatParamType.ts @@ -11,6 +11,7 @@ * * SeparatorTab: Can not be combined with SeparatorSpace and SeparatorSemicolon. And only applicable for csv output format. * * SeparatorSpace: Can not be combined with SeparatorTab and SeparatorSemicolon. And only applicable for csv output format. * * SeparatorSemicolon: Can not be combined with SeparatorTab and SeparatorSpace. And only applicable for csv output format. + * * ExcludeZerosAndMissingValues: Can be used by all formats but only have effect on csv, html and xlsx output format. * */ export enum OutputFormatParamType { @@ -21,4 +22,5 @@ export enum OutputFormatParamType { SEPARATOR_TAB = 'SeparatorTab', SEPARATOR_SPACE = 'SeparatorSpace', SEPARATOR_SEMICOLON = 'SeparatorSemicolon', + EXCLUDE_ZEROS_AND_MISSING_VALUES = 'ExcludeZerosAndMissingValues', } diff --git a/packages/pxweb2-api-client/src/models/jsonstat_extension_link.ts b/packages/pxweb2-api-client/src/models/jsonstat_extension_link.ts index b85284f9e..0c431cff5 100644 --- a/packages/pxweb2-api-client/src/models/jsonstat_extension_link.ts +++ b/packages/pxweb2-api-client/src/models/jsonstat_extension_link.ts @@ -2,12 +2,40 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ +import type { href } from './href'; +import type { label } from './label'; export type jsonstat_extension_link = { + /** + * DeprecationWarning, please do not use items from describedby, use items from related instead + * @deprecated + */ describedby?: Array<{ /** * A extension object */ extension?: Record; }>; + related?: Array<{ + extension: { + /** + * What type of information is the link to ( e.g. about-statistics, statistics-homepage, definition). Like the IANA relations, but for px. + */ + relation: string; + /** + * Non-null if the link applies to a spesific category. (Typically each contents variable has it own definition, in these cases category holds the contents variable.) + */ + category?: string | null; + /** + * Metaid that was the source when creating this Link + */ + metaid: string; + }; + href: href; + label: label; + /** + * Content-Type + */ + type: string; + }>; }; diff --git a/packages/pxweb2-api-client/src/models/jsonstat_link.ts b/packages/pxweb2-api-client/src/models/jsonstat_link.ts index e362efe96..c695c6ed6 100644 --- a/packages/pxweb2-api-client/src/models/jsonstat_link.ts +++ b/packages/pxweb2-api-client/src/models/jsonstat_link.ts @@ -3,6 +3,10 @@ /* tslint:disable */ /* eslint-disable */ import type { href } from './href'; +/** + * DeprecationWarning, please do not use jsonstat-link, use jsonstat-extension-link instead + * @deprecated + */ export type jsonstat_link = Record Date: Wed, 29 Apr 2026 13:27:47 +0200 Subject: [PATCH 02/12] Cleanup old, temp, commented code for definitions parsing --- .../src/mappers/JsonStat2ResponseMapper.ts | 138 +----------------- 1 file changed, 2 insertions(+), 136 deletions(-) diff --git a/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.ts b/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.ts index 6862306a0..9cddc76f7 100644 --- a/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.ts +++ b/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.ts @@ -31,143 +31,9 @@ import { } from '@pxweb2/pxweb2-ui'; import { getLabelText } from '../app/util/utils'; -// NOSONAR: Example temporary data for definitions mapping, remove when real data is available from API -// TODO: Remove when real data is available from API -//const tempMetaidLinksDataEmpty = {}; -// TODO: Remove temporary data when real data is available from API -// const tempMetaidLinksData = { -// 'about-statistics': { -// // currently "definisjoner og forklaringer" -// 'dataset-links': [ -// { -// metaid: 'KORTNAVN:aku', -// href: 'https://www.ssb.no/befolkning/folketall/statistikk/befolkning#om-statistikken', -// label: 'About the statistics', -// type: 'text/html', -// }, -// ], -// }, -// }; -// const tempMetaidLinksDataExtended = { -// // TODO: Do these two links only contain one item each? They are arrays in the temp data -// // which ones should be the "main" link that all tables should have (if they have anything in Definitions)? -// 'statistics-homepage': { -// //currently "statistikkside" -// 'dataset-links': [ -// { -// metaid: 'KORTNAVN:aku', -// href: 'https://www.ssb.no/befolkning/folketall/statistikk/befolkning', -// label: 'Statistics homepage', -// type: 'text/html', -// }, -// ], -// }, -// 'about-statistics': { -// // currently "definisjoner og forklaringer" -// 'dataset-links': [ -// { -// metaid: 'KORTNAVN:aku', -// href: 'https://www.ssb.no/befolkning/folketall/statistikk/befolkning#om-statistikken', -// label: 'About the statistics', -// type: 'text/html', -// }, -// ], -// }, -// definitions: { -// KOKkommuneregion0000: { -// 'dimension-links': [ -// { -// metaid: 'urn:ssb:classification:klass:231', -// href: 'https://www.ssb.no/klass/klassifikasjoner/231', -// label: 'Classification for region.', -// type: 'text/html', -// }, -// ], -// }, -// ContentsCode: { -// 'category-links': { -// KOSKBDU0000: [ -// { -// href: 'https://www.ssb.no/contextvariable/KOSKBDU0000', -// label: 'Korrigerte brutto driftsutgifter (1000 kr)', -// type: 'text/html', -// metaid: -// 'urn:ssb:contextvariable:common:8c42e415-e5dc-4a47-93bf-c9c515b39aa6:104549:KOSKBDU0000', -// }, -// ], -// KOSKBDUperelev0000: [ -// { -// href: 'https://www.ssb.no/contextvariable/KOSKBDUperelev0000', -// label: 'Korrigerte brutto driftsutgifter per elev (kr)', -// type: 'text/html', -// metaid: -// 'urn:ssb:contextvariable:common:8c42e415-e5dc-4a47-93bf-c9c515b39aa6:104549:KOSKBDUperelev0000', -// }, -// ], -// KOSKBDUperskyss0000: [ -// { -// href: 'https://www.ssb.no/contextvariable/KOSKBDUperskyss0000', -// label: -// 'Korrigerte brutto driftsutgifter per elev som får skoleskyss (223) (kr)', -// type: 'text/html', -// metaid: -// 'urn:ssb:contextvariable:common:8c42e415-e5dc-4a47-93bf-c9c515b39aa6:104549:KOSKBDUperskyss0000', -// }, -// ], -// }, -// }, -// }, -// }; - -// TODO: Remove TEMPORARY function to map raw JSON definitions data to Definitions type -// when real data is available from API -// TODO: Use the correct Response type from the API when available -// TODO: This needs a refactor when real data is available from API, quick and dirty for now -function mapTableDefinitions() { +function mapDefinitions() { const definitions: Definitions = {}; - // NOSONAR: Disabled sonar warning for unused code below, as this is temporary code - // until real data is available from the API - // definitionsJson['statistics-homepage'] && - // (definitions.statisticsHomepage = - // definitionsJson['statistics-homepage']['dataset-links'][0] || []); - - // NOSONAR: Disabled sonar warning for unused code below, as this is temporary code - // definitionsJson['about-statistics'] && - // (definitions.statisticsDefinitions = - // definitionsJson['about-statistics']['dataset-links'][0] || []); - - // NOSONAR: Disabled sonar warning for unused code below, as this is temporary code - // Object.keys(definitionsJson.definitions || {}).forEach((dimensionKey) => { - // const dimensionData = definitionsJson.definitions[dimensionKey]; - // const variableDefinition: VariableDefinition = { - // variableName: dimensionKey, - // links: [], - // }; - - // NOSONAR: Disabled sonar warning for unused code below, as this is temporary code - // if (dimensionData['dimension-links']) { - // variableDefinition.links.push(...dimensionData['dimension-links']); - // } - - // NOSONAR: Disabled sonar warning for unused code below, as this is temporary code - // if (dimensionData['category-links']) { - // Object.values(dimensionData['category-links']).forEach( - // (categoryLinks: DefinitionLink[]) => { - // variableDefinition.links.push(...categoryLinks); - // }, - // ); - // } - - // NOSONAR: Disabled sonar warning for unused code below, as this is temporary code - // if (!definitions.variablesDefinitions) { - // definitions.variablesDefinitions = []; - // } - - // NOSONAR: Disabled sonar warning for unused code below, as this is temporary code - // definitions.variablesDefinitions.push(variableDefinition); - // }); - return definitions; } @@ -230,7 +96,7 @@ export function mapJsonStat2Response( subjectArea: response.extension?.px?.['subject-area'] ?? '', variables: mapVariables(response, mapData), contacts: mapContacts(response.extension?.contact), - definitions: mapTableDefinitions(), // TODO: Use real data from API response when available + definitions: mapDefinitions(), // TODO: Use real data from API response when available notes: mapNotes(response.note, response.extension?.noteMandatory), pathElements: undefined, }; From 25fb9e09ecc70f44c2081013138802853d10a361 Mon Sep 17 00:00:00 2001 From: Sjur Sutterud Sagen Date: Wed, 29 Apr 2026 15:11:49 +0200 Subject: [PATCH 03/12] Add mapping step for definitions Still need to figure out the state handling, since when i console log what is returned it returns correctly, and then runs again (for the data?) and empties the state with the definitions. Co-authored-by: Copilot --- .../mappers/JsonStat2ResponseMapper.spec.ts | 454 ++++++++++++++++++ .../src/mappers/JsonStat2ResponseMapper.ts | 56 ++- 2 files changed, 508 insertions(+), 2 deletions(-) diff --git a/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.spec.ts b/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.spec.ts index 073e37940..fea1a498c 100644 --- a/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.spec.ts +++ b/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.spec.ts @@ -238,6 +238,460 @@ describe('JsonStat2ResponseMapper', () => { pxTable.metadata.variables[2].values[1].contentInfo?.decimals, ).equals(1); }); + + it('maps table-level and variable-level definitions from related links', () => { + const datasetWithDefinitions: Dataset = { + class: ClassType.DATASET, + version: Dataset.version._2_0, + label: 'Definitions table', + dimension: { + region: { + label: 'Region', + category: { + index: { N: 0 }, + label: { N: 'North' }, + }, + link: { + related: [ + { + extension: { + relation: 'definition', + metaid: 'var-region-1', + }, + href: 'https://example.com/definitions/region-1', + label: 'Region definition 1', + type: 'text/html', + }, + { + extension: { + relation: 'definition', + metaid: 'var-region-2', + }, + href: 'https://example.com/definitions/region-2', + label: 'Region definition 2', + type: 'text/html', + }, + ], + }, + }, + age: { + category: { + index: { A: 0 }, + label: { A: 'All ages' }, + }, + link: { + related: [ + { + extension: { + relation: 'definition', + metaid: 'var-age-1', + }, + href: 'https://example.com/definitions/age-1', + label: 'Age definition', + type: 'text/html', + }, + ], + }, + }, + }, + id: ['region', 'age'], + size: [1, 1], + value: [1], + link: { + related: [ + { + extension: { + relation: 'statistics-homepage', + metaid: 'table-home', + }, + href: 'https://example.com/statistics-homepage', + label: 'Statistics homepage', + type: 'text/html', + }, + { + extension: { + relation: 'about-statistics', + metaid: 'table-about', + }, + href: 'https://example.com/about-statistics', + label: 'About statistics', + type: 'text/html', + }, + ], + }, + }; + + const mapped = mapJsonStat2Response(datasetWithDefinitions); + + expect(mapped.metadata.definitions).toEqual({ + statisticsHomepage: { + href: 'https://example.com/statistics-homepage', + label: 'Statistics homepage', + type: 'text/html', + metaid: 'table-home', + }, + statisticsDefinitions: { + href: 'https://example.com/about-statistics', + label: 'About statistics', + type: 'text/html', + metaid: 'table-about', + }, + variablesDefinitions: [ + { + variableName: 'Region', + links: [ + { + href: 'https://example.com/definitions/region-1', + label: 'Region definition 1', + type: 'text/html', + metaid: 'var-region-1', + }, + { + href: 'https://example.com/definitions/region-2', + label: 'Region definition 2', + type: 'text/html', + metaid: 'var-region-2', + }, + ], + }, + { + variableName: 'age', + links: [ + { + href: 'https://example.com/definitions/age-1', + label: 'Age definition', + type: 'text/html', + metaid: 'var-age-1', + }, + ], + }, + ], + }); + }); + + it('filters unrelated relations and keeps first table singleton link', () => { + const datasetWithUnrelatedLinks: Dataset = { + class: ClassType.DATASET, + version: Dataset.version._2_0, + dimension: { + measure: { + label: 'Measure', + category: { + index: { M: 0 }, + label: { M: 'Measure' }, + }, + link: { + related: [ + { + extension: { relation: 'about-statistics', metaid: 'skip-var' }, + href: 'https://example.com/skip-variable', + label: 'Skip variable relation', + type: 'text/html', + }, + { + extension: { relation: 'definition', metaid: 'keep-var' }, + href: 'https://example.com/keep-variable', + label: 'Keep variable definition', + type: 'text/html', + }, + ], + }, + }, + }, + id: ['measure'], + size: [1], + value: [1], + link: { + related: [ + { + extension: { + relation: 'statistics-homepage', + metaid: 'home-first', + }, + href: 'https://example.com/home-first', + label: 'First homepage', + type: 'text/html', + }, + { + extension: { + relation: 'statistics-homepage', + metaid: 'home-second', + }, + href: 'https://example.com/home-second', + label: 'Second homepage', + type: 'text/html', + }, + { + extension: { + relation: 'about-statistics', + metaid: 'about-first', + }, + href: 'https://example.com/about-first', + label: 'First about', + type: 'text/html', + }, + { + extension: { + relation: 'about-statistics', + metaid: 'about-second', + }, + href: 'https://example.com/about-second', + label: 'Second about', + type: 'text/html', + }, + { + extension: { + relation: 'definition', + metaid: 'ignore-table-definition', + }, + href: 'https://example.com/ignore-table-definition', + label: 'Ignored table definition', + type: 'text/html', + }, + ], + }, + }; + + const mapped = mapJsonStat2Response(datasetWithUnrelatedLinks); + + expect(mapped.metadata.definitions.statisticsHomepage).toEqual({ + href: 'https://example.com/home-first', + label: 'First homepage', + type: 'text/html', + metaid: 'home-first', + }); + expect(mapped.metadata.definitions.statisticsDefinitions).toEqual({ + href: 'https://example.com/about-first', + label: 'First about', + type: 'text/html', + metaid: 'about-first', + }); + expect(mapped.metadata.definitions.variablesDefinitions).toEqual([ + { + variableName: 'Measure', + links: [ + { + href: 'https://example.com/keep-variable', + label: 'Keep variable definition', + type: 'text/html', + metaid: 'keep-var', + }, + ], + }, + ]); + }); + + it('returns empty definitions safely when related links are missing or empty', () => { + const withoutLinks: Dataset = { + class: ClassType.DATASET, + version: Dataset.version._2_0, + dimension: { + region: { + label: 'Region', + category: { + index: { N: 0 }, + label: { N: 'North' }, + }, + }, + }, + id: ['region'], + size: [1], + value: [1], + }; + + const withEmptyRelated: Dataset = { + ...withoutLinks, + link: { related: [] }, + dimension: { + region: { + ...withoutLinks.dimension.region, + link: { related: [] }, + }, + }, + }; + + expect(mapJsonStat2Response(withoutLinks).metadata.definitions).toEqual({}); + expect(mapJsonStat2Response(withEmptyRelated).metadata.definitions).toEqual( + {}, + ); + }); + + it('maps definitions independently across sequential calls', () => { + const firstDataset: Dataset = { + class: ClassType.DATASET, + version: Dataset.version._2_0, + dimension: { + region: { + label: 'Region', + category: { + index: { N: 0 }, + label: { N: 'North' }, + }, + link: { + related: [ + { + extension: { relation: 'definition', metaid: 'var-first' }, + href: 'https://example.com/var-first', + label: 'Variable first', + type: 'text/html', + }, + ], + }, + }, + }, + id: ['region'], + size: [1], + value: [1], + link: { + related: [ + { + extension: { + relation: 'about-statistics', + metaid: 'about-first', + }, + href: 'https://example.com/about-first', + label: 'About first', + type: 'text/html', + }, + ], + }, + }; + + const secondDataset: Dataset = { + class: ClassType.DATASET, + version: Dataset.version._2_0, + dimension: { + time: { + label: 'Time', + category: { + index: { '2025': 0 }, + label: { '2025': '2025' }, + }, + }, + }, + id: ['time'], + size: [1], + value: [2], + link: { + related: [ + { + extension: { + relation: 'statistics-homepage', + metaid: 'home-second', + }, + href: 'https://example.com/home-second', + label: 'Home second', + type: 'text/html', + }, + ], + }, + }; + + const firstMapped = mapJsonStat2Response(firstDataset); + const secondMapped = mapJsonStat2Response(secondDataset); + + expect(firstMapped.metadata.definitions).toEqual({ + statisticsDefinitions: { + href: 'https://example.com/about-first', + label: 'About first', + type: 'text/html', + metaid: 'about-first', + }, + variablesDefinitions: [ + { + variableName: 'Region', + links: [ + { + href: 'https://example.com/var-first', + label: 'Variable first', + type: 'text/html', + metaid: 'var-first', + }, + ], + }, + ], + }); + + expect(secondMapped.metadata.definitions).toEqual({ + statisticsHomepage: { + href: 'https://example.com/home-second', + label: 'Home second', + type: 'text/html', + metaid: 'home-second', + }, + }); + }); + + it('maps definitions for both mapData=true and mapData=false', () => { + const definitionsDataset: Dataset = { + class: ClassType.DATASET, + version: Dataset.version._2_0, + dimension: { + contentsCode: { + label: 'Contents', + category: { + index: { C1: 0 }, + label: { C1: 'Count' }, + }, + link: { + related: [ + { + extension: { relation: 'definition', metaid: 'var-content' }, + href: 'https://example.com/var-content', + label: 'Content definition', + type: 'text/html', + }, + ], + }, + }, + }, + role: { metric: ['contentsCode'] }, + id: ['contentsCode'], + size: [1], + value: [42], + link: { + related: [ + { + extension: { + relation: 'about-statistics', + metaid: 'about-both', + }, + href: 'https://example.com/about-both', + label: 'About both', + type: 'text/html', + }, + ], + }, + }; + + const mappedWithData = mapJsonStat2Response(definitionsDataset, true); + const mappedWithoutData = mapJsonStat2Response(definitionsDataset, false); + + expect(mappedWithData.metadata.definitions).toEqual( + mappedWithoutData.metadata.definitions, + ); + expect(mappedWithData.metadata.definitions).toEqual({ + statisticsDefinitions: { + href: 'https://example.com/about-both', + label: 'About both', + type: 'text/html', + metaid: 'about-both', + }, + variablesDefinitions: [ + { + variableName: 'Contents', + links: [ + { + href: 'https://example.com/var-content', + label: 'Content definition', + type: 'text/html', + metaid: 'var-content', + }, + ], + }, + ], + }); + }); }); describe('createDataAndStatus', () => { it('should map values and statuses correctly when both are provided', () => { diff --git a/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.ts b/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.ts index 9cddc76f7..135b7c3ce 100644 --- a/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.ts +++ b/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.ts @@ -31,12 +31,64 @@ import { } from '@pxweb2/pxweb2-ui'; import { getLabelText } from '../app/util/utils'; -function mapDefinitions() { +function mapDefinitions( + tableLinks: jsonstat_extension_link['related'] | undefined, + dimensions: Dataset['dimension'] | undefined, +): Definitions { const definitions: Definitions = {}; + for (const relatedLink of tableLinks ?? []) { + const relation = relatedLink.extension.relation; + if ( + relation === 'statistics-homepage' && + definitions.statisticsHomepage === undefined + ) { + definitions.statisticsHomepage = mapDefinitionLink(relatedLink); + } else if ( + relation === 'about-statistics' && + definitions.statisticsDefinitions === undefined + ) { + definitions.statisticsDefinitions = mapDefinitionLink(relatedLink); + } + } + + const variablesDefinitions = Object.entries(dimensions ?? {}).reduce< + NonNullable + >((acc, [dimensionId, dimension]) => { + const definitionLinks = (dimension.link?.related ?? []) + .filter((relatedLink) => relatedLink.extension.relation === 'definitions') + .map(mapDefinitionLink); + + if (definitionLinks.length > 0) { + acc.push({ + variableName: dimension.label ?? dimensionId, + links: definitionLinks, + }); + } + + return acc; + }, []); + + if (variablesDefinitions.length > 0) { + definitions.variablesDefinitions = variablesDefinitions; + } + + console.log('Mapped definitions:', definitions); + return definitions; } +function mapDefinitionLink( + relatedLink: NonNullable[number], +) { + return { + href: relatedLink.href, + label: relatedLink.label, + type: relatedLink.type, + metaid: relatedLink.extension.metaid, + }; +} + /** * Internal type. Used to keep track of index in json-stat2 value array * Need to be an object to be passed by reference @@ -96,7 +148,7 @@ export function mapJsonStat2Response( subjectArea: response.extension?.px?.['subject-area'] ?? '', variables: mapVariables(response, mapData), contacts: mapContacts(response.extension?.contact), - definitions: mapDefinitions(), // TODO: Use real data from API response when available + definitions: mapDefinitions(response.link?.related, response.dimension), notes: mapNotes(response.note, response.extension?.noteMandatory), pathElements: undefined, }; From 843387e4a345c8feb1890eebc91b220a117565a7 Mon Sep 17 00:00:00 2001 From: Sjur Sutterud Sagen Date: Thu, 30 Apr 2026 11:43:09 +0200 Subject: [PATCH 04/12] Update mapper to parse definitions in the metadata --- .../mappers/JsonStat2ResponseMapper.spec.ts | 124 ++---------------- .../src/mappers/JsonStat2ResponseMapper.ts | 11 +- 2 files changed, 15 insertions(+), 120 deletions(-) diff --git a/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.spec.ts b/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.spec.ts index fea1a498c..45e3a544e 100644 --- a/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.spec.ts +++ b/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.spec.ts @@ -369,118 +369,6 @@ describe('JsonStat2ResponseMapper', () => { }); }); - it('filters unrelated relations and keeps first table singleton link', () => { - const datasetWithUnrelatedLinks: Dataset = { - class: ClassType.DATASET, - version: Dataset.version._2_0, - dimension: { - measure: { - label: 'Measure', - category: { - index: { M: 0 }, - label: { M: 'Measure' }, - }, - link: { - related: [ - { - extension: { relation: 'about-statistics', metaid: 'skip-var' }, - href: 'https://example.com/skip-variable', - label: 'Skip variable relation', - type: 'text/html', - }, - { - extension: { relation: 'definition', metaid: 'keep-var' }, - href: 'https://example.com/keep-variable', - label: 'Keep variable definition', - type: 'text/html', - }, - ], - }, - }, - }, - id: ['measure'], - size: [1], - value: [1], - link: { - related: [ - { - extension: { - relation: 'statistics-homepage', - metaid: 'home-first', - }, - href: 'https://example.com/home-first', - label: 'First homepage', - type: 'text/html', - }, - { - extension: { - relation: 'statistics-homepage', - metaid: 'home-second', - }, - href: 'https://example.com/home-second', - label: 'Second homepage', - type: 'text/html', - }, - { - extension: { - relation: 'about-statistics', - metaid: 'about-first', - }, - href: 'https://example.com/about-first', - label: 'First about', - type: 'text/html', - }, - { - extension: { - relation: 'about-statistics', - metaid: 'about-second', - }, - href: 'https://example.com/about-second', - label: 'Second about', - type: 'text/html', - }, - { - extension: { - relation: 'definition', - metaid: 'ignore-table-definition', - }, - href: 'https://example.com/ignore-table-definition', - label: 'Ignored table definition', - type: 'text/html', - }, - ], - }, - }; - - const mapped = mapJsonStat2Response(datasetWithUnrelatedLinks); - - expect(mapped.metadata.definitions.statisticsHomepage).toEqual({ - href: 'https://example.com/home-first', - label: 'First homepage', - type: 'text/html', - metaid: 'home-first', - }); - expect(mapped.metadata.definitions.statisticsDefinitions).toEqual({ - href: 'https://example.com/about-first', - label: 'First about', - type: 'text/html', - metaid: 'about-first', - }); - expect(mapped.metadata.definitions.variablesDefinitions).toEqual([ - { - variableName: 'Measure', - links: [ - { - href: 'https://example.com/keep-variable', - label: 'Keep variable definition', - type: 'text/html', - metaid: 'keep-var', - }, - ], - }, - ]); - }); - it('returns empty definitions safely when related links are missing or empty', () => { const withoutLinks: Dataset = { class: ClassType.DATASET, @@ -510,10 +398,12 @@ describe('JsonStat2ResponseMapper', () => { }, }; - expect(mapJsonStat2Response(withoutLinks).metadata.definitions).toEqual({}); - expect(mapJsonStat2Response(withEmptyRelated).metadata.definitions).toEqual( + expect(mapJsonStat2Response(withoutLinks).metadata.definitions).toEqual( {}, ); + expect( + mapJsonStat2Response(withEmptyRelated).metadata.definitions, + ).toEqual({}); }); it('maps definitions independently across sequential calls', () => { @@ -622,6 +512,8 @@ describe('JsonStat2ResponseMapper', () => { }); }); + //TODO: This should not be run? Only metadata calls has definitions? + // This is a possible fault in the current implementation that should be fixed in this PR it('maps definitions for both mapData=true and mapData=false', () => { const definitionsDataset: Dataset = { class: ClassType.DATASET, @@ -693,6 +585,7 @@ describe('JsonStat2ResponseMapper', () => { }); }); }); + describe('createDataAndStatus', () => { it('should map values and statuses correctly when both are provided', () => { // Arrange @@ -837,6 +730,7 @@ describe('JsonStat2ResponseMapper', () => { ], contacts: [], notes: [], + definitions: {}, }; const data: PxTableData = { @@ -916,6 +810,7 @@ describe('JsonStat2ResponseMapper', () => { ], contacts: [], notes: [], + definitions: {}, }; const data: PxTableData = { @@ -982,6 +877,7 @@ describe('JsonStat2ResponseMapper', () => { ], contacts: [], notes: [], + definitions: {}, }; const data: PxTableData = { diff --git a/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.ts b/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.ts index 135b7c3ce..49256eab6 100644 --- a/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.ts +++ b/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.ts @@ -44,7 +44,8 @@ function mapDefinitions( definitions.statisticsHomepage === undefined ) { definitions.statisticsHomepage = mapDefinitionLink(relatedLink); - } else if ( + } + if ( relation === 'about-statistics' && definitions.statisticsDefinitions === undefined ) { @@ -55,9 +56,9 @@ function mapDefinitions( const variablesDefinitions = Object.entries(dimensions ?? {}).reduce< NonNullable >((acc, [dimensionId, dimension]) => { - const definitionLinks = (dimension.link?.related ?? []) - .filter((relatedLink) => relatedLink.extension.relation === 'definitions') - .map(mapDefinitionLink); + const definitionLinks = (dimension.link?.related ?? []).map( + mapDefinitionLink, + ); if (definitionLinks.length > 0) { acc.push({ @@ -73,8 +74,6 @@ function mapDefinitions( definitions.variablesDefinitions = variablesDefinitions; } - console.log('Mapped definitions:', definitions); - return definitions; } From 9eca83a9b9bd054579204275dd9175f2a4acecc2 Mon Sep 17 00:00:00 2001 From: Sjur Sutterud Sagen Date: Thu, 30 Apr 2026 11:46:00 +0200 Subject: [PATCH 05/12] Update definitions tab to use data from API response --- .../TableInformation.spec.tsx | 57 ++++++++++--------- .../TableInformation/TableInformation.tsx | 8 ++- 2 files changed, 35 insertions(+), 30 deletions(-) diff --git a/packages/pxweb2/src/app/components/TableInformation/TableInformation.spec.tsx b/packages/pxweb2/src/app/components/TableInformation/TableInformation.spec.tsx index 0aa41fdd1..5dd8787eb 100644 --- a/packages/pxweb2/src/app/components/TableInformation/TableInformation.spec.tsx +++ b/packages/pxweb2/src/app/components/TableInformation/TableInformation.spec.tsx @@ -9,9 +9,9 @@ import { mockHTMLDialogElement } from '@pxweb2/pxweb2-ui/src/lib/util/test-utils import { renderWithProviders } from '../../util/testing-utils'; import { AppContext, AppContextType } from '../../context/AppProvider'; import { - TableDataContext, - TableDataContextType, -} from '../../context/TableDataProvider'; + VariablesContext, + VariablesContextType, +} from '../../context/VariablesProvider'; describe('TableInformation', () => { beforeEach(() => { @@ -155,35 +155,38 @@ describe('TableInformation', () => { expect(definitionsTab).not.toBeInTheDocument(); }); - it('should render Definitions tab when definitions exist', () => { - const tableDataContextValue: TableDataContextType = { + it('should render Definitions tab when variables metadata contains definitions', () => { + const variablesContextValue: VariablesContextType = { isInitialized: true, - data: { - metadata: { - definitions: { - statisticsDefinitions: { - href: 'https://example.com/definitions', - label: 'Definitions', - }, + pxTableMetadata: { + definitions: { + statisticsDefinitions: { + href: 'https://example.com/from-metadata-call', + label: 'Definitions from metadata call', + type: 'text/html', }, }, - } as unknown as TableDataContextType['data'], - fetchTableData: vi.fn(), - fetchSavedQuery: vi.fn(), - pivotToMobile: vi.fn(), - pivotToDesktop: vi.fn(), - pivot: vi.fn(), - buildTableTitle: vi.fn().mockReturnValue({ - contentText: '', - firstTitlePart: '', - lastTitlePart: '', - }), - isFadingTable: false, - setIsFadingTable: vi.fn(), + } as unknown as VariablesContextType['pxTableMetadata'], + setPxTableMetadata: vi.fn(), + addSelectedValues: vi.fn(), + getSelectedValuesById: vi.fn().mockReturnValue([]), + getSelectedValuesByIdSorted: vi.fn().mockReturnValue([]), + getSelectedCodelistById: vi.fn().mockReturnValue(undefined), + getNumberOfSelectedValues: vi.fn().mockReturnValue(0), + getSelectedMatrixSize: vi.fn().mockReturnValue(1), + getUniqueIds: vi.fn().mockReturnValue([]), + syncVariablesAndValues: vi.fn(), + hasLoadedInitialSelection: false, + setHasLoadedInitialSelection: vi.fn(), + setSelectedVBValues: vi.fn(), + selectedVBValues: [], + isMatrixSizeAllowed: true, + isLoadingMetadata: false, + setIsLoadingMetadata: vi.fn(), }; renderWithProviders( - + { return; }} /> - , + , ); const definitionsTab = screen.getByRole('tab', { diff --git a/packages/pxweb2/src/app/components/TableInformation/TableInformation.tsx b/packages/pxweb2/src/app/components/TableInformation/TableInformation.tsx index 522984aaa..3ccaaa7cf 100644 --- a/packages/pxweb2/src/app/components/TableInformation/TableInformation.tsx +++ b/packages/pxweb2/src/app/components/TableInformation/TableInformation.tsx @@ -4,6 +4,7 @@ import cl from 'clsx'; import classes from './TableInformation.module.scss'; import useTableData from '../../context/useTableData'; +import useVariables from '../../context/useVariables'; import { ContactTab } from './Contact/ContactTab'; import { DetailsTab } from './Details/DetailsTab'; import useApp from '../../context/useApp'; @@ -32,7 +33,8 @@ export function TableInformation({ }: TableInformationProps) { const { t } = useTranslation(); const [activeTab, setActiveTab] = useState(selectedTab ?? ''); - const metadataOrUndefined = useTableData().data?.metadata; + const metadataOrUndefined = useTableData().data?.metadata; // metadata only for chosen values + const definitionsOrUndefined = useVariables().pxTableMetadata?.definitions; // total metadata, narrowed down to definitions const { isMobile } = useApp(); const tabsContentRef = useRef(null); @@ -49,7 +51,7 @@ export function TableInformation({ const tabsVariant = isMobile ? 'scrollable' : 'fixed'; const definitionsMandatoryLinkExists = - metadataOrUndefined?.definitions?.statisticsDefinitions !== undefined; + definitionsOrUndefined?.statisticsDefinitions !== undefined; return ( {definitionsMandatoryLinkExists && ( - + )} From 07bfbf1108af3dd94b0d0c3cb3ea26b9a48a43f0 Mon Sep 17 00:00:00 2001 From: Sjur Sutterud Sagen Date: Thu, 30 Apr 2026 12:25:21 +0200 Subject: [PATCH 06/12] Remove comment --- packages/pxweb2/src/mappers/JsonStat2ResponseMapper.spec.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.spec.ts b/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.spec.ts index 45e3a544e..ee4da1837 100644 --- a/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.spec.ts +++ b/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.spec.ts @@ -512,8 +512,6 @@ describe('JsonStat2ResponseMapper', () => { }); }); - //TODO: This should not be run? Only metadata calls has definitions? - // This is a possible fault in the current implementation that should be fixed in this PR it('maps definitions for both mapData=true and mapData=false', () => { const definitionsDataset: Dataset = { class: ClassType.DATASET, From 897d8cd80a73416e3bcac6b057dbe115262ad66f Mon Sep 17 00:00:00 2001 From: Sjur Sutterud Sagen Date: Thu, 30 Apr 2026 12:56:56 +0200 Subject: [PATCH 07/12] Update API URL in aleternative deployment configuration --- .github/workflows/pull-request.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 468dd5ec4..c63a71b80 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -125,6 +125,8 @@ jobs: curl -s -o dist/statbank/config/config.js https://www.ssb.no/statbank/config/config.js sed -i 's| Date: Mon, 4 May 2026 10:30:35 +0200 Subject: [PATCH 08/12] Capitalize the first letter of variable names in mapDefinitions function --- packages/pxweb2/src/mappers/JsonStat2ResponseMapper.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.ts b/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.ts index 49256eab6..91de66945 100644 --- a/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.ts +++ b/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.ts @@ -61,8 +61,11 @@ function mapDefinitions( ); if (definitionLinks.length > 0) { + const variableName = dimension.label ?? dimensionId; + acc.push({ - variableName: dimension.label ?? dimensionId, + variableName: + variableName.charAt(0).toUpperCase() + variableName.slice(1), // Capitalize first letter links: definitionLinks, }); } From d6ffd0bc12063d567aabcef83d9b3f0485089d20 Mon Sep 17 00:00:00 2001 From: Sjur Sutterud Sagen Date: Mon, 4 May 2026 10:32:53 +0200 Subject: [PATCH 09/12] Update test for definitions mapping with capitalized variable names --- packages/pxweb2/src/mappers/JsonStat2ResponseMapper.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.spec.ts b/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.spec.ts index ee4da1837..4cc185916 100644 --- a/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.spec.ts +++ b/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.spec.ts @@ -355,7 +355,7 @@ describe('JsonStat2ResponseMapper', () => { ], }, { - variableName: 'age', + variableName: 'Age', links: [ { href: 'https://example.com/definitions/age-1', From f299029a0a1a3c542233f3bda2615b8849bad86c Mon Sep 17 00:00:00 2001 From: Sjur Sutterud Sagen Date: Mon, 4 May 2026 13:05:33 +0200 Subject: [PATCH 10/12] Capitalize variable names in Metadata component and simplify conditionals in mapDefinitions function --- .../Definitions/Metadata/Metadata.tsx | 6 +++++- .../pxweb2/src/mappers/JsonStat2ResponseMapper.ts | 13 +++---------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/packages/pxweb2/src/app/components/TableInformation/Definitions/Metadata/Metadata.tsx b/packages/pxweb2/src/app/components/TableInformation/Definitions/Metadata/Metadata.tsx index 6d656fdd2..df3d574c4 100644 --- a/packages/pxweb2/src/app/components/TableInformation/Definitions/Metadata/Metadata.tsx +++ b/packages/pxweb2/src/app/components/TableInformation/Definitions/Metadata/Metadata.tsx @@ -37,7 +37,11 @@ export function Metadata({ variablesDefinitions }: MetadataProps) { >
- {variable.variableName} + { + // Capitalize the first letter of the variable name + variable.variableName.charAt(0).toUpperCase() + + variable.variableName.slice(1) + }
diff --git a/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.ts b/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.ts index 91de66945..20d3e2cc0 100644 --- a/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.ts +++ b/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.ts @@ -39,16 +39,10 @@ function mapDefinitions( for (const relatedLink of tableLinks ?? []) { const relation = relatedLink.extension.relation; - if ( - relation === 'statistics-homepage' && - definitions.statisticsHomepage === undefined - ) { + if (relation === 'statistics-homepage') { definitions.statisticsHomepage = mapDefinitionLink(relatedLink); } - if ( - relation === 'about-statistics' && - definitions.statisticsDefinitions === undefined - ) { + if (relation === 'about-statistics') { definitions.statisticsDefinitions = mapDefinitionLink(relatedLink); } } @@ -64,8 +58,7 @@ function mapDefinitions( const variableName = dimension.label ?? dimensionId; acc.push({ - variableName: - variableName.charAt(0).toUpperCase() + variableName.slice(1), // Capitalize first letter + variableName: variableName, links: definitionLinks, }); } From 3c703c9df97def158e57a3f8a5a9d7bd6aed3f33 Mon Sep 17 00:00:00 2001 From: Sjur Sutterud Sagen Date: Mon, 4 May 2026 13:08:10 +0200 Subject: [PATCH 11/12] Undo variableName capitalization in test for mapper --- packages/pxweb2/src/mappers/JsonStat2ResponseMapper.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.spec.ts b/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.spec.ts index 4cc185916..ee4da1837 100644 --- a/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.spec.ts +++ b/packages/pxweb2/src/mappers/JsonStat2ResponseMapper.spec.ts @@ -355,7 +355,7 @@ describe('JsonStat2ResponseMapper', () => { ], }, { - variableName: 'Age', + variableName: 'age', links: [ { href: 'https://example.com/definitions/age-1', From 41f1140f1884d49f0d85ba45c196e8c7b6ecd7b4 Mon Sep 17 00:00:00 2001 From: Sjur Sutterud Sagen Date: Mon, 11 May 2026 14:29:27 +0200 Subject: [PATCH 12/12] Remove temp API URL configuration from build steps in pull request workflow --- .github/workflows/pull-request.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index c63a71b80..468dd5ec4 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -125,8 +125,6 @@ jobs: curl -s -o dist/statbank/config/config.js https://www.ssb.no/statbank/config/config.js sed -i 's|