From 3086bb5ecd0f295f48539d293fb9d77efee39254 Mon Sep 17 00:00:00 2001 From: Miu Razvan Date: Wed, 20 May 2026 10:10:15 +0300 Subject: [PATCH 1/7] fix: embed data table only includes columns used by chart traces --- src/PlotlyComponent/PlotlyComponent.jsx | 69 +++++++++++++++---------- 1 file changed, 43 insertions(+), 26 deletions(-) diff --git a/src/PlotlyComponent/PlotlyComponent.jsx b/src/PlotlyComponent/PlotlyComponent.jsx index 7efb37d..5d93f7f 100644 --- a/src/PlotlyComponent/PlotlyComponent.jsx +++ b/src/PlotlyComponent/PlotlyComponent.jsx @@ -1,38 +1,38 @@ -import { useMemo, useRef, useEffect, useState, useCallback } from 'react'; -import { useIntl } from 'react-intl'; -import { compose } from 'redux'; -import { connect } from 'react-redux'; -import isArray from 'lodash/isArray'; -import uniqBy from 'lodash/uniqBy'; -import sortBy from 'lodash/sortBy'; -import isNil from 'lodash/isNil'; -import debounce from 'lodash/debounce'; -import cx from 'classnames'; -import { FormField } from 'semantic-ui-react'; // import { constants } from '@eeacms/react-chart-editor'; -import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable'; -import { flattenToAppURL } from '@plone/volto/helpers/Url/Url'; import { VisibilitySensor } from '@eeacms/volto-datablocks/components'; import { connectToProviderData } from '@eeacms/volto-datablocks/hocs'; -import { connectBlockToVisualization } from '@eeacms/volto-plotlycharts/hocs'; +import { Toolbar } from '@eeacms/volto-plotlycharts/Utils'; +import { + getMetadataFlags, + processMetadataArrays, +} from '@eeacms/volto-plotlycharts/Utils/utils'; import { deleteGeneratedFigureMetadataBlock, - getFigurePosition, getFigureMetadata, + getFigurePosition, insertFigureMetadataBeforeBlock, } from '@eeacms/volto-plotlycharts/helpers'; import { - updateTrace, updateDataSources, + updateTrace, } from '@eeacms/volto-plotlycharts/helpers/plotly'; import { applyFilters } from '@eeacms/volto-plotlycharts/helpers/transforms'; -import { Toolbar } from '@eeacms/volto-plotlycharts/Utils'; -import Plot from './Plot'; +import { connectBlockToVisualization } from '@eeacms/volto-plotlycharts/hocs'; +import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable'; +import { flattenToAppURL } from '@plone/volto/helpers/Url/Url'; +import cx from 'classnames'; +import debounce from 'lodash/debounce'; +import isArray from 'lodash/isArray'; +import isNil from 'lodash/isNil'; +import sortBy from 'lodash/sortBy'; +import uniqBy from 'lodash/uniqBy'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useIntl } from 'react-intl'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; +import { FormField } from 'semantic-ui-react'; import Placeholder from './Placeholder'; -import { - getMetadataFlags, - processMetadataArrays, -} from '@eeacms/volto-plotlycharts/Utils/utils'; +import Plot from './Plot'; // generateCSVForDataset, // generateOriginalCSV, @@ -460,16 +460,32 @@ function UnconnectedPlotlyComponent(props) { ); } -function prepareEmbedData(dataSources, provider_metadata, core_metadata) { - let array = []; +function prepareEmbedData( + dataSources, + traces, + provider_metadata, + core_metadata, +) { + const array = []; + const srcKeys = traces.reduce((acc, trace) => { + Object.keys(trace).forEach((key) => { + if (key.endsWith('src')) { + if (!acc.includes(trace[key])) { + acc.push(trace[key]); + } + } + }); + return acc; + }, []); Object.entries(dataSources).forEach(([key, items]) => { + if (!srcKeys.includes(key)) return; items.forEach((item, index) => { if (!array[index]) array[index] = {}; array[index][key] = item; }); }); - let readme = provider_metadata?.readme ? [provider_metadata?.readme] : []; + const readme = provider_metadata?.readme ? [provider_metadata?.readme] : []; const metadataFlags = getMetadataFlags(core_metadata); const metadataArrays = processMetadataArrays(core_metadata, metadataFlags); @@ -504,7 +520,7 @@ function Table({ rows }) { function EmbedData(props) { const { provider_metadata, content, block } = props; // reactChartEditorLib, const visualization = props.data?.visualization || {}; - const { dataSources = {}, layout } = visualization; + const { dataSources = {}, layout, data: traces = [] } = visualization; const { data_provenance, @@ -524,6 +540,7 @@ function EmbedData(props) { const embedData = prepareEmbedData( dataSources, + traces, provider_metadata, core_metadata, ); From db5bdb8e3635be352b5c72fe9a902df2bd5228b3 Mon Sep 17 00:00:00 2001 From: Miu Razvan Date: Wed, 20 May 2026 11:52:00 +0300 Subject: [PATCH 2/7] Pin @plone/scripts to 3.x --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index fa04d3d..2473b61 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "@eeacms/volto-datablocks": "*", "@eeacms/volto-embed": "*", "@eeacms/volto-matomo": "*", - "@plone/scripts": "*", + "@plone/scripts": "^3.10.6", "handsontable": "^15.2.0", "jsoneditor": "10.2.0", "jszip": "3.10.1", @@ -39,7 +39,7 @@ }, "devDependencies": { "@cypress/code-coverage": "^3.10.0", - "@plone/scripts": "*", + "@plone/scripts": "^3.10.6", "babel-plugin-transform-class-properties": "^6.24.1", "cypress": "13.1.0", "dotenv": "^16.3.2", From 0790ba3abb17be707d4b7bcf1102ff3db28752b7 Mon Sep 17 00:00:00 2001 From: Miu Razvan Date: Fri, 22 May 2026 03:41:55 +0300 Subject: [PATCH 3/7] feat(PlotlyComponent): structure figure-info for indexing + embed LLM summary --- src/PlotlyComponent/PlotlyComponent.jsx | 162 +++++++++++++----------- 1 file changed, 90 insertions(+), 72 deletions(-) diff --git a/src/PlotlyComponent/PlotlyComponent.jsx b/src/PlotlyComponent/PlotlyComponent.jsx index 5d93f7f..9d1844f 100644 --- a/src/PlotlyComponent/PlotlyComponent.jsx +++ b/src/PlotlyComponent/PlotlyComponent.jsx @@ -58,6 +58,11 @@ function stripHtml(html) { return doc.body.textContent || ''; } +// Only null/undefined/blank strings are empty. `0` and `false` are real data. +function isEmptyValue(value) { + return isNil(value) || (typeof value === 'string' && value.trim() === ''); +} + function UnconnectedPlotlyComponent(props) { const intl = useIntl(); const { reactChartEditor } = props; @@ -446,16 +451,6 @@ function UnconnectedPlotlyComponent(props) { data={toolbarData} provider_metadata={provider_metadata} /> - - {llm_summary && ( - - )} ); } @@ -489,36 +484,54 @@ function prepareEmbedData( const metadataFlags = getMetadataFlags(core_metadata); const metadataArrays = processMetadataArrays(core_metadata, metadataFlags); - return { array, readme, metadataArrays, metadataFlags }; + return { array, readme, metadataArrays }; } -function Table({ rows }) { - const stableKeys = Object.keys(rows?.[0] || {}); +// Column-major rendering: one line per column, values comma-joined in their +// original row order so positional meaning is preserved. Blank cells become "-", +// fully-empty columns are dropped. Generic — also used for metadata tables. +function Table({ rows, title }) { + if (!isArray(rows) || rows.length === 0) return null; + + // Union of keys across all rows, preserving first-appearance order. + const keys = rows.reduce((acc, row) => { + Object.keys(row || {}).forEach((key) => { + if (!acc.includes(key)) acc.push(key); + }); + return acc; + }, []); + + const columns = keys + .map((key) => { + const values = rows.map((row) => + isEmptyValue(row?.[key]) ? '-' : String(row[key]), + ); + // Drop columns that carry no data at all. + if (values.every((value) => value === '-')) return null; + return { key, text: values.join(', ') }; + }) + .filter(Boolean); + + // No renderable column -> render nothing, including the heading. This keeps + // the heading and its content as a single visibility decision. + if (columns.length === 0) return null; return ( - - - - {stableKeys.map((key) => ( - - ))} - - - - {rows.map((row, index) => ( - - {stableKeys.map((key) => ( - - ))} - + <> + {title &&

{title}:

} +
    + {columns.map((column) => ( +
  • + {column.key}: {column.text} +
  • ))} -
-
{key}
{row[key]}
+ + ); } function EmbedData(props) { - const { provider_metadata, content, block } = props; // reactChartEditorLib, + const { provider_metadata, block } = props; // reactChartEditorLib, const visualization = props.data?.visualization || {}; const { dataSources = {}, layout, data: traces = [] } = visualization; @@ -544,62 +557,67 @@ function EmbedData(props) { provider_metadata, core_metadata, ); - const { array, readme, metadataArrays, metadataFlags } = embedData; + const { array, readme, metadataArrays } = embedData; + + const subtitle = stripHtml(layout?.title?.subtitle?.text || ''); + const llmSummary = stripHtml(props.data?.llm_summary || ''); return (
-

{content.title}

{!!layout?.title?.text && ( -

{stripHtml(layout.title.text)}

+

+ Visualization title: {stripHtml(layout.title.text)} +

)} - {!!layout?.title?.subtitle?.text && ( -

- {stripHtml(layout.title.subtitle.text)} -

+ {!!subtitle && subtitle !== layout?.yaxis?.title?.text && ( +

+ {layout?.yaxis?.title?.text + ? 'Subtitle: ' + : 'Secondary label (shown as chart subtitle; may also serve as the y-axis title): '} + {subtitle} +

)} + {!!llmSummary &&

Summary: {llmSummary}

} {!!layout?.xaxis?.title?.text && ( -

{stripHtml(layout.xaxis.title.text)}

+

+ X axis title: {stripHtml(layout.xaxis.title.text)} +

)} {!!layout?.yaxis?.title?.text && ( -

{stripHtml(layout.yaxis.title.text)}

+

+ Y axis title: {stripHtml(layout.yaxis.title.text)} +

)} - +

+ In each data section below, every line is one column: its values are + listed in row order and align by position across columns — the Nth value + in every column belongs to the same record. "-" means no value. +

- {metadataFlags.hasDataProvenance && ( - <> -

Data Provenance

-
- - )} - {metadataFlags.hasOtherOrganisation && ( - <> -

Other Organisations

-
- - )} - {metadataFlags.hasTemporalCoverage && ( - <> -

Temporal Coverage

-
- - )} - {metadataFlags.hasGeoCoverage && ( - <> -

Geographical Coverage

-
- - )} - {metadataFlags.hasPublisher && ( - <> -

Publisher

-
- - )} +
+ +
+
+
+
+
{readme}
From 27b484a5c7278e26f2d31a057ab115d6d1f60343 Mon Sep 17 00:00:00 2001 From: Miu Razvan Date: Mon, 25 May 2026 12:26:20 +0300 Subject: [PATCH 4/7] remove unnused variable --- src/PlotlyComponent/PlotlyComponent.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PlotlyComponent/PlotlyComponent.jsx b/src/PlotlyComponent/PlotlyComponent.jsx index 9d1844f..12210b5 100644 --- a/src/PlotlyComponent/PlotlyComponent.jsx +++ b/src/PlotlyComponent/PlotlyComponent.jsx @@ -90,8 +90,7 @@ function UnconnectedPlotlyComponent(props) { const { height, vis_url, - with_metadata_section = true, - llm_summary, + with_metadata_section = true } = props.data; const [initialized, setInitialized] = useState(false); const [filtersState, setFiltersState] = useState([]); From bf22b4af2f6a359c510cd6a8330de3282758b5bb Mon Sep 17 00:00:00 2001 From: eea-jenkins Date: Mon, 25 May 2026 11:30:08 +0200 Subject: [PATCH 5/7] style: Automated code fix --- src/PlotlyComponent/PlotlyComponent.jsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/PlotlyComponent/PlotlyComponent.jsx b/src/PlotlyComponent/PlotlyComponent.jsx index 12210b5..73704b8 100644 --- a/src/PlotlyComponent/PlotlyComponent.jsx +++ b/src/PlotlyComponent/PlotlyComponent.jsx @@ -87,11 +87,7 @@ function UnconnectedPlotlyComponent(props) { onDeleteBlock, blocksConfig, } = props; - const { - height, - vis_url, - with_metadata_section = true - } = props.data; + const { height, vis_url, with_metadata_section = true } = props.data; const [initialized, setInitialized] = useState(false); const [filtersState, setFiltersState] = useState([]); const [autoscaleHeight, setAutoscaleHeight] = useState(null); From d9385aa94dd090ed50dad69f73f65b4c8e597c9b Mon Sep 17 00:00:00 2001 From: Miu Razvan Date: Mon, 25 May 2026 17:50:39 +0300 Subject: [PATCH 6/7] Add unit tests --- src/Utils/utils.test.js | 189 ++++++++++++++++++++ src/actions.test.js | 26 +++ src/helpers/editor.test.js | 60 +++++++ src/middlewares/data_visualizations.test.js | 58 ++++++ src/reducers/data_visualizations.test.js | 108 +++++++++++ 5 files changed, 441 insertions(+) create mode 100644 src/Utils/utils.test.js create mode 100644 src/actions.test.js create mode 100644 src/helpers/editor.test.js create mode 100644 src/middlewares/data_visualizations.test.js create mode 100644 src/reducers/data_visualizations.test.js diff --git a/src/Utils/utils.test.js b/src/Utils/utils.test.js new file mode 100644 index 0000000..cd98e07 --- /dev/null +++ b/src/Utils/utils.test.js @@ -0,0 +1,189 @@ +import { + generateFinalCSV, + generateOriginalCSV, + getMetadataFlags, + processMetadataArrays, + generateCSVForDataset, +} from './utils'; + +jest.mock('@eeacms/volto-matomo/utils', () => ({ + trackLink: jest.fn(), +})); + +const emptyCoreMetadata = {}; + +describe('getMetadataFlags', () => { + it('should be all false for empty metadata', () => { + expect(getMetadataFlags(emptyCoreMetadata)).toEqual({ + hasDataProvenance: false, + hasOtherOrganisation: false, + hasTemporalCoverage: false, + hasGeoCoverage: false, + hasPublisher: false, + }); + }); + + it('should flag present metadata sections', () => { + expect( + getMetadataFlags({ + data_provenance: ['Source A'], + publisher: ['EEA'], + }), + ).toMatchObject({ + hasDataProvenance: true, + hasPublisher: true, + hasOtherOrganisation: false, + }); + }); +}); + +describe('processMetadataArrays', () => { + it('should return empty arrays when no flag is set', () => { + const flags = getMetadataFlags(emptyCoreMetadata); + expect(processMetadataArrays(emptyCoreMetadata, flags)).toEqual({ + data_provenance_array: [], + other_organisation_array: [], + temporal_coverage_array: [], + geo_coverage_array: [], + publisher_array: [], + }); + }); + + it('should populate arrays for flagged sections', () => { + const core_metadata = { + data_provenance: ['Source A', 'Source B'], + }; + const flags = getMetadataFlags(core_metadata); + const result = processMetadataArrays(core_metadata, flags); + + expect(result.data_provenance_array.length).toBeGreaterThan(0); + // each entry keyed by a column that includes "data_provenance" or "Sources" + result.data_provenance_array.forEach((row) => { + Object.keys(row).forEach((key) => { + expect(key.includes('data_provenance') || key.includes('Sources')).toBe( + true, + ); + }); + }); + }); +}); + +describe('generateFinalCSV', () => { + it('should concatenate the download source line and data with no metadata', () => { + const flags = getMetadataFlags(emptyCoreMetadata); + const arrays = processMetadataArrays(emptyCoreMetadata, flags); + + const csv = generateFinalCSV( + [{ region: 'EU', value: 10 }], + [], + flags, + arrays, + 'http://example.org', + ); + + expect(csv).toContain('Downloaded from: '); + expect(csv).toContain('http://example.org'); + expect(csv).toContain('region,value'); + expect(csv).toContain('EU'); + }); + + it('should include metadata sections when flags are set', () => { + const core_metadata = { publisher: ['EEA'] }; + const flags = getMetadataFlags(core_metadata); + const arrays = processMetadataArrays(core_metadata, flags); + + const csv = generateFinalCSV( + [{ region: 'EU', value: 10 }], + [], + flags, + arrays, + 'http://example.org', + ); + + expect(csv).toContain('EEA'); + }); +}); + +describe('generateOriginalCSV', () => { + it('should build CSV from data sources without columns', () => { + const csv = generateOriginalCSV( + { region: ['EU', 'US'], value: [10, 20] }, + [], + {}, + 'http://example.org', + emptyCoreMetadata, + ); + + expect(csv).toContain('region,value'); + expect(csv).toContain('EU'); + expect(csv).toContain('US'); + }); + + it('should order columns according to the provided columns list', () => { + const csv = generateOriginalCSV( + { value: [10], region: ['EU'] }, + ['region', 'value'], + {}, + 'http://example.org', + emptyCoreMetadata, + ); + + expect(csv).toContain('region,value'); + }); + + it('should prepend the readme from provider metadata', () => { + const csv = generateOriginalCSV( + { region: ['EU'] }, + [], + { readme: 'Read this' }, + 'http://example.org', + emptyCoreMetadata, + ); + + expect(csv).toContain('Read this'); + }); +}); + +describe('generateCSVForDataset', () => { + const reactChartEditorLib = { + constants: { TRACE_SRC_ATTRIBUTES: ['x', 'y'] }, + getAttrsPath: (trace) => { + const attrs = {}; + if (trace.x) attrs.x = trace.x; + if (trace.y) attrs.y = trace.y; + return attrs; + }, + getSrcAttr: (trace, attr) => ({ value: trace[`${attr}src`] }), + }; + + it('should build CSV from trace-processed data', () => { + const csv = generateCSVForDataset( + { year: [2020, 2021], region: ['EU', 'US'] }, + { + data: [{ x: [2020, 2021], xsrc: 'year' }], + layout: {}, + }, + {}, + emptyCoreMetadata, + 'http://example.org', + reactChartEditorLib, + ); + + expect(csv).toContain('year'); + expect(csv).toContain('2020'); + }); + + it('should fall back to data sources when traces yield no data', () => { + const csv = generateCSVForDataset( + { region: ['EU', 'US'], value: [10, 20] }, + { data: [], layout: {} }, + {}, + emptyCoreMetadata, + 'http://example.org', + reactChartEditorLib, + ); + + expect(csv).toContain('region,value'); + expect(csv).toContain('EU'); + }); +}); diff --git a/src/actions.test.js b/src/actions.test.js new file mode 100644 index 0000000..f40d095 --- /dev/null +++ b/src/actions.test.js @@ -0,0 +1,26 @@ +import { getVisualization, removeVisualization } from './actions'; +import { GET_VISUALIZATION, REMOVE_VISUALIZATION } from './constants'; + +describe('actions', () => { + describe('getVisualization', () => { + it('should build a GET_VISUALIZATION request action', () => { + expect(getVisualization('/path/to/chart')).toEqual({ + type: GET_VISUALIZATION, + path: '/path/to/chart', + request: { + op: 'get', + path: '/path/to/chart/@visualization', + }, + }); + }); + }); + + describe('removeVisualization', () => { + it('should build a REMOVE_VISUALIZATION action', () => { + expect(removeVisualization('/path/to/chart')).toEqual({ + type: REMOVE_VISUALIZATION, + path: '/path/to/chart', + }); + }); + }); +}); diff --git a/src/helpers/editor.test.js b/src/helpers/editor.test.js new file mode 100644 index 0000000..ec57806 --- /dev/null +++ b/src/helpers/editor.test.js @@ -0,0 +1,60 @@ +import { destroyEditor, onPasteEditor, validateEditor } from './editor'; + +jest.mock('react-toastify', () => ({ + toast: { warn: jest.fn() }, +})); + +jest.mock('@plone/volto/components/manage/Toast/Toast', () => () => null); + +describe('editor helpers', () => { + describe('destroyEditor', () => { + it('should call destroy on a provided editor', () => { + const editor = { destroy: jest.fn() }; + destroyEditor(editor); + expect(editor.destroy).toHaveBeenCalled(); + }); + + it('should do nothing when no editor is given', () => { + expect(() => destroyEditor(null)).not.toThrow(); + }); + }); + + describe('onPasteEditor', () => { + it('should repair and format the editor', () => { + const editor = { + current: { repair: jest.fn(), format: jest.fn() }, + }; + onPasteEditor(editor); + expect(editor.current.repair).toHaveBeenCalled(); + expect(editor.current.format).toHaveBeenCalled(); + }); + + it('should swallow errors thrown while repairing', () => { + const editor = { + current: { + repair: () => { + throw new Error('bad'); + }, + format: jest.fn(), + }, + }; + expect(() => onPasteEditor(editor)).not.toThrow(); + }); + }); + + describe('validateEditor', () => { + it('should return true when there are no validation errors', async () => { + const editor = { current: { validate: jest.fn().mockResolvedValue([]) } }; + await expect(validateEditor(editor)).resolves.toBe(true); + }); + + it('should warn and return false when validation errors exist', async () => { + const { toast } = require('react-toastify'); + const editor = { + current: { validate: jest.fn().mockResolvedValue([{ message: 'x' }]) }, + }; + await expect(validateEditor(editor)).resolves.toBe(false); + expect(toast.warn).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/middlewares/data_visualizations.test.js b/src/middlewares/data_visualizations.test.js new file mode 100644 index 0000000..392b6f9 --- /dev/null +++ b/src/middlewares/data_visualizations.test.js @@ -0,0 +1,58 @@ +import { data_visualizations } from './data_visualizations'; +import { GET_VISUALIZATION } from '@eeacms/volto-plotlycharts/constants'; + +const runMiddleware = (action, state) => { + const dispatch = jest.fn(); + const next = jest.fn((a) => a); + const store = { + getState: () => state, + dispatch, + }; + const [mw] = data_visualizations([]); + const result = mw(store)(next)(action); + return { dispatch, next, result }; +}; + +describe('data_visualizations middleware', () => { + it('should append the provided middlewares', () => { + const extra = () => {}; + const chain = data_visualizations([extra]); + expect(chain).toHaveLength(2); + expect(chain[1]).toBe(extra); + }); + + it('should dispatch a PENDING action and forward when not already pending', () => { + const { dispatch, next } = runMiddleware( + { type: GET_VISUALIZATION, path: '/chart' }, + { data_visualizations: { pendingVisualizations: {} } }, + ); + + expect(dispatch).toHaveBeenCalledWith({ + type: `${GET_VISUALIZATION}_PENDING`, + path: '/chart', + }); + expect(next).toHaveBeenCalled(); + }); + + it('should short-circuit when the path is already pending', () => { + const { dispatch, next, result } = runMiddleware( + { type: GET_VISUALIZATION, path: '/chart' }, + { data_visualizations: { pendingVisualizations: { '/chart': true } } }, + ); + + expect(dispatch).not.toHaveBeenCalled(); + expect(next).not.toHaveBeenCalled(); + expect(result).toBeUndefined(); + }); + + it('should forward unrelated actions untouched', () => { + const action = { type: 'SOMETHING_ELSE' }; + const { dispatch, next, result } = runMiddleware(action, { + data_visualizations: { pendingVisualizations: {} }, + }); + + expect(dispatch).not.toHaveBeenCalled(); + expect(next).toHaveBeenCalledWith(action); + expect(result).toBe(action); + }); +}); diff --git a/src/reducers/data_visualizations.test.js b/src/reducers/data_visualizations.test.js new file mode 100644 index 0000000..feb1b24 --- /dev/null +++ b/src/reducers/data_visualizations.test.js @@ -0,0 +1,108 @@ +import data_visualizations from './data_visualizations'; +import { + GET_VISUALIZATION, + REMOVE_VISUALIZATION, +} from '@eeacms/volto-plotlycharts/constants'; + +const initialState = { + data: {}, + error: null, + loaded: false, + loading: false, + pendingVisualizations: {}, + failedVisualizations: {}, + requested: [], +}; + +describe('data_visualizations reducer', () => { + it('should return the initial state by default', () => { + expect(data_visualizations(undefined, {})).toEqual(initialState); + }); + + it('should mark a path pending on GET_VISUALIZATION_PENDING', () => { + const state = data_visualizations(undefined, { + type: `${GET_VISUALIZATION}_PENDING`, + path: '/chart/@visualization', + }); + + expect(state.loading).toBe(true); + expect(state.loaded).toBe(false); + expect(state.error).toBe(null); + expect(state.requested).toEqual(['/chart']); + expect(state.pendingVisualizations).toEqual({ '/chart': true }); + }); + + it('should clear a failed flag when a pending request restarts', () => { + const prev = { + ...initialState, + failedVisualizations: { '/chart': true }, + }; + const state = data_visualizations(prev, { + type: `${GET_VISUALIZATION}_PENDING`, + path: '/chart/@visualization', + }); + + expect(state.failedVisualizations).toEqual({}); + }); + + it('should store the result on GET_VISUALIZATION_SUCCESS', () => { + const prev = { + ...initialState, + requested: ['/chart'], + pendingVisualizations: { '/chart': true }, + }; + const state = data_visualizations(prev, { + type: `${GET_VISUALIZATION}_SUCCESS`, + path: '/chart/@visualization', + result: { foo: 'bar' }, + }); + + expect(state.loaded).toBe(true); + expect(state.loading).toBe(false); + expect(state.data).toEqual({ '/chart': { foo: 'bar' } }); + expect(state.requested).toEqual([]); + expect(state.pendingVisualizations).toEqual({}); + }); + + it('should record the error and failed flag on GET_VISUALIZATION_FAIL', () => { + const prev = { + ...initialState, + requested: ['/chart'], + pendingVisualizations: { '/chart': true }, + }; + const state = data_visualizations(prev, { + type: `${GET_VISUALIZATION}_FAIL`, + path: '/chart/@visualization', + error: 'boom', + }); + + expect(state.error).toBe('boom'); + expect(state.loaded).toBe(false); + expect(state.loading).toBe(false); + expect(state.requested).toEqual([]); + expect(state.pendingVisualizations).toEqual({}); + expect(state.failedVisualizations).toEqual({ '/chart': true }); + }); + + it('should drop stored data on REMOVE_VISUALIZATION', () => { + const prev = { + ...initialState, + data: { '/chart': { foo: 'bar' }, '/other': { baz: 1 } }, + }; + const state = data_visualizations(prev, { + type: REMOVE_VISUALIZATION, + path: '/chart', + }); + + expect(state.data).toEqual({ '/other': { baz: 1 } }); + }); + + it('should handle actions without a path', () => { + const state = data_visualizations(undefined, { + type: `${GET_VISUALIZATION}_PENDING`, + }); + + expect(state.requested).toEqual([undefined]); + expect(state.pendingVisualizations).toEqual({ undefined: true }); + }); +}); From 6c3895a1b34b9f0755bac83808983c70b9618bc3 Mon Sep 17 00:00:00 2001 From: EEA Jenkins <@users.noreply.github.com> Date: Mon, 25 May 2026 14:58:39 +0000 Subject: [PATCH 7/7] Automated release 14.0.4 --- CHANGELOG.md | 19 +++++++++++++++++++ package.json | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3da84f..8d2b63b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,25 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +### [14.0.4](https://github.com/eea/volto-plotlycharts/compare/14.0.3...14.0.4) - 25 May 2026 + +#### :rocket: New Features + +- feat(PlotlyComponent): structure figure-info for indexing + embed LLM summary [Miu Razvan - [`0790ba3`](https://github.com/eea/volto-plotlycharts/commit/0790ba3abb17be707d4b7bcf1102ff3db28752b7)] + +#### :bug: Bug Fixes + +- fix: embed data table only includes columns used by chart traces [Miu Razvan - [`3086bb5`](https://github.com/eea/volto-plotlycharts/commit/3086bb5ecd0f295f48539d293fb9d77efee39254)] + +#### :house: Internal changes + +- style: Automated code fix [eea-jenkins - [`bf22b4a`](https://github.com/eea/volto-plotlycharts/commit/bf22b4af2f6a359c510cd6a8330de3282758b5bb)] + +#### :hammer_and_wrench: Others + +- Add unit tests [Miu Razvan - [`d9385aa`](https://github.com/eea/volto-plotlycharts/commit/d9385aa94dd090ed50dad69f73f65b4c8e597c9b)] +- remove unnused variable [Miu Razvan - [`27b484a`](https://github.com/eea/volto-plotlycharts/commit/27b484a5c7278e26f2d31a057ab115d6d1f60343)] +- Pin @plone/scripts to 3.x [Miu Razvan - [`db5bdb8`](https://github.com/eea/volto-plotlycharts/commit/db5bdb8e3635be352b5c72fe9a902df2bd5228b3)] ### [14.0.3](https://github.com/eea/volto-plotlycharts/compare/14.0.2...14.0.3) - 22 April 2026 #### :hammer_and_wrench: Others diff --git a/package.json b/package.json index 2473b61..10e8058 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@eeacms/volto-plotlycharts", - "version": "14.0.3", + "version": "14.0.4", "description": "Plotly Charts and Editor integration for Volto", "main": "src/index.js", "author": "European Environment Agency: IDM2 A-Team",