@carto/ps-react-ui 4.7.0 → 4.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/category-DwaeYjpX.js +656 -0
- package/dist/category-DwaeYjpX.js.map +1 -0
- package/dist/change-column-Cidl_M-4.js +1110 -0
- package/dist/change-column-Cidl_M-4.js.map +1 -0
- package/dist/data-zoom-layout-BH0LPwSy.js +28 -0
- package/dist/data-zoom-layout-BH0LPwSy.js.map +1 -0
- package/dist/echart-CU0KmClP.js +176 -0
- package/dist/echart-CU0KmClP.js.map +1 -0
- package/dist/exports-Cx-f6m6U.js +63 -0
- package/dist/exports-Cx-f6m6U.js.map +1 -0
- package/dist/formula-DuC0NQLH.js +79 -0
- package/dist/formula-DuC0NQLH.js.map +1 -0
- package/dist/markdown-BD1jcknS.js +8326 -0
- package/dist/markdown-BD1jcknS.js.map +1 -0
- package/dist/merge-options-DCkkHZIf.js +34 -0
- package/dist/merge-options-DCkkHZIf.js.map +1 -0
- package/dist/{styles-BYTyKQFP.js → option-builders-F-c9ELi1.js} +25 -45
- package/dist/option-builders-F-c9ELi1.js.map +1 -0
- package/dist/png-item-CS4z1iSH.js +45 -0
- package/dist/png-item-CS4z1iSH.js.map +1 -0
- package/dist/range-DsqTjSpg.js +186 -0
- package/dist/range-DsqTjSpg.js.map +1 -0
- package/dist/spread-CTuIXZSM.js +67 -0
- package/dist/spread-CTuIXZSM.js.map +1 -0
- package/dist/style-DVnT6HC1.js +131 -0
- package/dist/style-DVnT6HC1.js.map +1 -0
- package/dist/styles-cohnxh9F.js +23 -0
- package/dist/styles-cohnxh9F.js.map +1 -0
- package/dist/table-HIpXuq4G.js +390 -0
- package/dist/table-HIpXuq4G.js.map +1 -0
- package/dist/transforms-Cdx4fkU5.js +106 -0
- package/dist/transforms-Cdx4fkU5.js.map +1 -0
- package/dist/types/widgets/echart/utils.test.d.ts +1 -0
- package/dist/types/widgets/formula/config.test.d.ts +1 -0
- package/dist/types/widgets/stores/widget-store-branches.test.d.ts +1 -0
- package/dist/types/widgets/table/config.test.d.ts +1 -0
- package/dist/types/widgets-v2/actions/brush-toggle/brush-toggle.d.ts +56 -0
- package/dist/types/widgets-v2/actions/brush-toggle/index.d.ts +3 -0
- package/dist/types/widgets-v2/actions/brush-toggle/labels.d.ts +5 -0
- package/dist/types/widgets-v2/actions/brush-toggle/style.d.ts +12 -0
- package/dist/types/widgets-v2/actions/brush-toggle/transforms.d.ts +11 -0
- package/dist/types/widgets-v2/actions/brush-toggle/transforms.test.d.ts +1 -0
- package/dist/types/widgets-v2/actions/change-column/change-column-icon.d.ts +2 -0
- package/dist/types/widgets-v2/actions/change-column/change-column.d.ts +29 -0
- package/dist/types/widgets-v2/actions/change-column/index.d.ts +3 -0
- package/dist/types/widgets-v2/actions/change-column/labels.d.ts +5 -0
- package/dist/types/widgets-v2/actions/change-column/sortable-column-item.d.ts +14 -0
- package/dist/types/widgets-v2/actions/change-column/style.d.ts +33 -0
- package/dist/types/widgets-v2/actions/change-column/types.d.ts +10 -0
- package/dist/types/widgets-v2/actions/download/download.d.ts +18 -0
- package/dist/types/widgets-v2/actions/download/exports.d.ts +37 -0
- package/dist/types/widgets-v2/actions/download/icons.d.ts +12 -0
- package/dist/types/widgets-v2/actions/download/index.d.ts +6 -0
- package/dist/types/widgets-v2/actions/download/labels.d.ts +11 -0
- package/dist/types/widgets-v2/actions/download/png-item.d.ts +24 -0
- package/dist/types/widgets-v2/actions/download/style.d.ts +1 -0
- package/dist/types/widgets-v2/actions/download/types.d.ts +35 -0
- package/dist/types/widgets-v2/actions/fullscreen/fullscreen.d.ts +59 -0
- package/dist/types/widgets-v2/actions/fullscreen/index.d.ts +3 -0
- package/dist/types/widgets-v2/actions/fullscreen/labels.d.ts +5 -0
- package/dist/types/widgets-v2/actions/fullscreen/style.d.ts +48 -0
- package/dist/types/widgets-v2/actions/fullscreen/types.d.ts +14 -0
- package/dist/types/widgets-v2/actions/index.d.ts +9 -0
- package/dist/types/widgets-v2/actions/lock-selection/index.d.ts +3 -0
- package/dist/types/widgets-v2/actions/lock-selection/labels.d.ts +6 -0
- package/dist/types/widgets-v2/actions/lock-selection/lock-selection.d.ts +36 -0
- package/dist/types/widgets-v2/actions/lock-selection/style.d.ts +12 -0
- package/dist/types/widgets-v2/actions/lock-selection/transforms.d.ts +6 -0
- package/dist/types/widgets-v2/actions/relative-data/index.d.ts +3 -0
- package/dist/types/widgets-v2/actions/relative-data/labels.d.ts +5 -0
- package/dist/types/widgets-v2/actions/relative-data/relative-data.d.ts +39 -0
- package/dist/types/widgets-v2/actions/relative-data/style.d.ts +12 -0
- package/dist/types/widgets-v2/actions/relative-data/transforms.d.ts +30 -0
- package/dist/types/widgets-v2/actions/relative-data/transforms.test.d.ts +1 -0
- package/dist/types/widgets-v2/actions/searcher/filter.d.ts +6 -0
- package/dist/types/widgets-v2/actions/searcher/index.d.ts +4 -0
- package/dist/types/widgets-v2/actions/searcher/labels.d.ts +7 -0
- package/dist/types/widgets-v2/actions/searcher/searcher-toggle.d.ts +23 -0
- package/dist/types/widgets-v2/actions/searcher/searcher.d.ts +11 -0
- package/dist/types/widgets-v2/actions/searcher/style.d.ts +16 -0
- package/dist/types/widgets-v2/actions/stack-toggle/index.d.ts +3 -0
- package/dist/types/widgets-v2/actions/stack-toggle/labels.d.ts +5 -0
- package/dist/types/widgets-v2/actions/stack-toggle/stack-toggle.d.ts +10 -0
- package/dist/types/widgets-v2/actions/stack-toggle/style.d.ts +12 -0
- package/dist/types/widgets-v2/actions/stack-toggle/transforms.d.ts +13 -0
- package/dist/types/widgets-v2/actions/stack-toggle/transforms.test.d.ts +1 -0
- package/dist/types/widgets-v2/actions/zoom-toggle/index.d.ts +3 -0
- package/dist/types/widgets-v2/actions/zoom-toggle/labels.d.ts +5 -0
- package/dist/types/widgets-v2/actions/zoom-toggle/style.d.ts +12 -0
- package/dist/types/widgets-v2/actions/zoom-toggle/transforms.d.ts +51 -0
- package/dist/types/widgets-v2/actions/zoom-toggle/transforms.test.d.ts +1 -0
- package/dist/types/widgets-v2/actions/zoom-toggle/zoom-toggle.d.ts +35 -0
- package/dist/types/widgets-v2/bar/download.d.ts +24 -0
- package/dist/types/widgets-v2/bar/index.d.ts +4 -0
- package/dist/types/widgets-v2/bar/options.d.ts +43 -0
- package/dist/types/widgets-v2/bar/options.test.d.ts +1 -0
- package/dist/types/widgets-v2/bar/skeleton.d.ts +6 -0
- package/dist/types/widgets-v2/bar/types.d.ts +41 -0
- package/dist/types/widgets-v2/category/category-ui.d.ts +81 -0
- package/dist/types/widgets-v2/category/category.d.ts +48 -0
- package/dist/types/widgets-v2/category/components/category-bar-stacked.d.ts +28 -0
- package/dist/types/widgets-v2/category/components/category-bar.d.ts +23 -0
- package/dist/types/widgets-v2/category/components/category-legend.d.ts +18 -0
- package/dist/types/widgets-v2/category/components/category-row-multi.d.ts +31 -0
- package/dist/types/widgets-v2/category/components/category-row-other.d.ts +13 -0
- package/dist/types/widgets-v2/category/components/category-row-single.d.ts +28 -0
- package/dist/types/widgets-v2/category/components/category-row-stacked.d.ts +38 -0
- package/dist/types/widgets-v2/category/download.d.ts +16 -0
- package/dist/types/widgets-v2/category/download.test.d.ts +1 -0
- package/dist/types/widgets-v2/category/index.d.ts +10 -0
- package/dist/types/widgets-v2/category/skeleton.d.ts +11 -0
- package/dist/types/widgets-v2/category/style.d.ts +166 -0
- package/dist/types/widgets-v2/category/types.d.ts +49 -0
- package/dist/types/widgets-v2/echart/echart-ui.d.ts +44 -0
- package/dist/types/widgets-v2/echart/echart.d.ts +75 -0
- package/dist/types/widgets-v2/echart/index.d.ts +4 -0
- package/dist/types/widgets-v2/echart/shared-resize-observer.d.ts +5 -0
- package/dist/types/widgets-v2/echart/shared-resize-observer.test.d.ts +1 -0
- package/dist/types/widgets-v2/echart/style.d.ts +6 -0
- package/dist/types/widgets-v2/echart/use-chart-selection.d.ts +51 -0
- package/dist/types/widgets-v2/formula/delta.d.ts +22 -0
- package/dist/types/widgets-v2/formula/download.d.ts +20 -0
- package/dist/types/widgets-v2/formula/formula-ui.d.ts +20 -0
- package/dist/types/widgets-v2/formula/formula.d.ts +8 -0
- package/dist/types/widgets-v2/formula/index.d.ts +11 -0
- package/dist/types/widgets-v2/formula/note.d.ts +11 -0
- package/dist/types/widgets-v2/formula/prefix.d.ts +12 -0
- package/dist/types/widgets-v2/formula/series.d.ts +16 -0
- package/dist/types/widgets-v2/formula/skeleton.d.ts +4 -0
- package/dist/types/widgets-v2/formula/style.d.ts +29 -0
- package/dist/types/widgets-v2/formula/suffix.d.ts +12 -0
- package/dist/types/widgets-v2/formula/types.d.ts +40 -0
- package/dist/types/widgets-v2/formula/value.d.ts +14 -0
- package/dist/types/widgets-v2/histogram/download.d.ts +17 -0
- package/dist/types/widgets-v2/histogram/download.test.d.ts +1 -0
- package/dist/types/widgets-v2/histogram/index.d.ts +5 -0
- package/dist/types/widgets-v2/histogram/options.d.ts +42 -0
- package/dist/types/widgets-v2/histogram/options.test.d.ts +1 -0
- package/dist/types/widgets-v2/histogram/skeleton.d.ts +9 -0
- package/dist/types/widgets-v2/histogram/transforms.d.ts +17 -0
- package/dist/types/widgets-v2/histogram/transforms.test.d.ts +1 -0
- package/dist/types/widgets-v2/histogram/types.d.ts +47 -0
- package/dist/types/widgets-v2/index.d.ts +107 -0
- package/dist/types/widgets-v2/markdown/download.d.ts +16 -0
- package/dist/types/widgets-v2/markdown/download.test.d.ts +1 -0
- package/dist/types/widgets-v2/markdown/index.d.ts +6 -0
- package/dist/types/widgets-v2/markdown/markdown-content.d.ts +34 -0
- package/dist/types/widgets-v2/markdown/markdown-ui.d.ts +12 -0
- package/dist/types/widgets-v2/markdown/markdown.d.ts +6 -0
- package/dist/types/widgets-v2/markdown/skeleton.d.ts +4 -0
- package/dist/types/widgets-v2/markdown/style.d.ts +61 -0
- package/dist/types/widgets-v2/markdown/types.d.ts +4 -0
- package/dist/types/widgets-v2/note/labels.d.ts +5 -0
- package/dist/types/widgets-v2/note/style.d.ts +26 -0
- package/dist/types/widgets-v2/note/widget-note.d.ts +46 -0
- package/dist/types/widgets-v2/pie/download.d.ts +17 -0
- package/dist/types/widgets-v2/pie/download.test.d.ts +1 -0
- package/dist/types/widgets-v2/pie/index.d.ts +4 -0
- package/dist/types/widgets-v2/pie/options.d.ts +35 -0
- package/dist/types/widgets-v2/pie/options.test.d.ts +1 -0
- package/dist/types/widgets-v2/pie/skeleton.d.ts +4 -0
- package/dist/types/widgets-v2/pie/types.d.ts +50 -0
- package/dist/types/widgets-v2/provider/widget-provider.d.ts +32 -0
- package/dist/types/widgets-v2/range/index.d.ts +4 -0
- package/dist/types/widgets-v2/range/range-ui.d.ts +19 -0
- package/dist/types/widgets-v2/range/range.d.ts +19 -0
- package/dist/types/widgets-v2/range/skeleton.d.ts +9 -0
- package/dist/types/widgets-v2/range/style.d.ts +40 -0
- package/dist/types/widgets-v2/range/types.d.ts +37 -0
- package/dist/types/widgets-v2/scatterplot/download.d.ts +16 -0
- package/dist/types/widgets-v2/scatterplot/download.test.d.ts +1 -0
- package/dist/types/widgets-v2/scatterplot/index.d.ts +5 -0
- package/dist/types/widgets-v2/scatterplot/options.d.ts +42 -0
- package/dist/types/widgets-v2/scatterplot/options.test.d.ts +1 -0
- package/dist/types/widgets-v2/scatterplot/skeleton.d.ts +12 -0
- package/dist/types/widgets-v2/scatterplot/transforms.d.ts +17 -0
- package/dist/types/widgets-v2/scatterplot/transforms.test.d.ts +1 -0
- package/dist/types/widgets-v2/scatterplot/types.d.ts +50 -0
- package/dist/types/widgets-v2/selection-summary/labels.d.ts +6 -0
- package/dist/types/widgets-v2/selection-summary/selection-summary.d.ts +22 -0
- package/dist/types/widgets-v2/selection-summary/style.d.ts +23 -0
- package/dist/types/widgets-v2/spread/download.d.ts +15 -0
- package/dist/types/widgets-v2/spread/download.test.d.ts +1 -0
- package/dist/types/widgets-v2/spread/index.d.ts +6 -0
- package/dist/types/widgets-v2/spread/separator.d.ts +7 -0
- package/dist/types/widgets-v2/spread/skeleton.d.ts +9 -0
- package/dist/types/widgets-v2/spread/spread-ui.d.ts +18 -0
- package/dist/types/widgets-v2/spread/spread.d.ts +5 -0
- package/dist/types/widgets-v2/spread/types.d.ts +25 -0
- package/dist/types/widgets-v2/state/labels.d.ts +7 -0
- package/dist/types/widgets-v2/state/labels.test.d.ts +1 -0
- package/dist/types/widgets-v2/state/style.d.ts +19 -0
- package/dist/types/widgets-v2/state/widget-state.d.ts +19 -0
- package/dist/types/widgets-v2/stores/index.d.ts +8 -0
- package/dist/types/widgets-v2/stores/pipeline-middleware.d.ts +5 -0
- package/dist/types/widgets-v2/stores/pipeline-middleware.test.d.ts +1 -0
- package/dist/types/widgets-v2/stores/transforms.d.ts +4 -0
- package/dist/types/widgets-v2/stores/transforms.test.d.ts +1 -0
- package/dist/types/widgets-v2/stores/types.d.ts +55 -0
- package/dist/types/widgets-v2/stores/use-echart-instance.d.ts +15 -0
- package/dist/types/widgets-v2/stores/use-transform-enabled.d.ts +17 -0
- package/dist/types/widgets-v2/stores/use-transform.d.ts +12 -0
- package/dist/types/widgets-v2/stores/widget-context.d.ts +2 -0
- package/dist/types/widgets-v2/stores/widget-store-registry.d.ts +74 -0
- package/dist/types/widgets-v2/stores/widget-store-registry.test.d.ts +1 -0
- package/dist/types/widgets-v2/subheader/style.d.ts +10 -0
- package/dist/types/widgets-v2/subheader/subheader.d.ts +11 -0
- package/dist/types/widgets-v2/table/download.d.ts +18 -0
- package/dist/types/widgets-v2/table/download.test.d.ts +1 -0
- package/dist/types/widgets-v2/table/helpers.d.ts +32 -0
- package/dist/types/widgets-v2/table/helpers.test.d.ts +1 -0
- package/dist/types/widgets-v2/table/index.d.ts +7 -0
- package/dist/types/widgets-v2/table/labels.d.ts +22 -0
- package/dist/types/widgets-v2/table/skeleton.d.ts +22 -0
- package/dist/types/widgets-v2/table/style.d.ts +44 -0
- package/dist/types/widgets-v2/table/table-ui.d.ts +38 -0
- package/dist/types/widgets-v2/table/table.d.ts +50 -0
- package/dist/types/widgets-v2/table/types.d.ts +37 -0
- package/dist/types/widgets-v2/test-utils.d.ts +52 -0
- package/dist/types/widgets-v2/timeseries/download.d.ts +17 -0
- package/dist/types/widgets-v2/timeseries/download.test.d.ts +1 -0
- package/dist/types/widgets-v2/timeseries/index.d.ts +4 -0
- package/dist/types/widgets-v2/timeseries/options.d.ts +39 -0
- package/dist/types/widgets-v2/timeseries/options.test.d.ts +1 -0
- package/dist/types/widgets-v2/timeseries/skeleton.d.ts +8 -0
- package/dist/types/widgets-v2/timeseries/types.d.ts +56 -0
- package/dist/types/widgets-v2/toolbox/labels.d.ts +5 -0
- package/dist/types/widgets-v2/toolbox/style.d.ts +30 -0
- package/dist/types/widgets-v2/toolbox/toolbox.d.ts +49 -0
- package/dist/types/widgets-v2/utils/data-zoom-layout.d.ts +11 -0
- package/dist/types/widgets-v2/utils/index.d.ts +2 -0
- package/dist/types/widgets-v2/utils/merge-options.d.ts +12 -0
- package/dist/types/widgets-v2/utils/merge-options.test.d.ts +1 -0
- package/dist/types/widgets-v2/wrapper/index.d.ts +4 -0
- package/dist/types/widgets-v2/wrapper/labels.d.ts +6 -0
- package/dist/types/widgets-v2/wrapper/style.d.ts +111 -0
- package/dist/types/widgets-v2/wrapper/widget-actions.d.ts +22 -0
- package/dist/types/widgets-v2/wrapper/widget-content.d.ts +12 -0
- package/dist/types/widgets-v2/wrapper/widget-wrapper.d.ts +51 -0
- package/dist/use-transform-DXPN3nY7.js +110 -0
- package/dist/use-transform-DXPN3nY7.js.map +1 -0
- package/dist/widget-context-DTGO0Yta.js +13 -0
- package/dist/widget-context-DTGO0Yta.js.map +1 -0
- package/dist/widget-store-registry-_W4Z4xp-.js +178 -0
- package/dist/widget-store-registry-_W4Z4xp-.js.map +1 -0
- package/dist/widgets/actions.js +688 -668
- package/dist/widgets/actions.js.map +1 -1
- package/dist/widgets/bar.js +14 -13
- package/dist/widgets/bar.js.map +1 -1
- package/dist/widgets/histogram.js +38 -37
- package/dist/widgets/histogram.js.map +1 -1
- package/dist/widgets/pie.js +19 -18
- package/dist/widgets/pie.js.map +1 -1
- package/dist/widgets/scatterplot.js +8 -7
- package/dist/widgets/scatterplot.js.map +1 -1
- package/dist/widgets/timeseries.js +11 -10
- package/dist/widgets/timeseries.js.map +1 -1
- package/dist/widgets/utils.js +8 -7
- package/dist/widgets/utils.js.map +1 -1
- package/dist/widgets-v2/actions.js +43 -0
- package/dist/widgets-v2/actions.js.map +1 -0
- package/dist/widgets-v2/bar.js +327 -0
- package/dist/widgets-v2/bar.js.map +1 -0
- package/dist/widgets-v2/category.js +104 -0
- package/dist/widgets-v2/category.js.map +1 -0
- package/dist/widgets-v2/echart.js +57 -0
- package/dist/widgets-v2/echart.js.map +1 -0
- package/dist/widgets-v2/formula.js +74 -0
- package/dist/widgets-v2/formula.js.map +1 -0
- package/dist/widgets-v2/histogram.js +350 -0
- package/dist/widgets-v2/histogram.js.map +1 -0
- package/dist/widgets-v2/markdown.js +68 -0
- package/dist/widgets-v2/markdown.js.map +1 -0
- package/dist/widgets-v2/pie.js +381 -0
- package/dist/widgets-v2/pie.js.map +1 -0
- package/dist/widgets-v2/range.js +52 -0
- package/dist/widgets-v2/range.js.map +1 -0
- package/dist/widgets-v2/scatterplot.js +405 -0
- package/dist/widgets-v2/scatterplot.js.map +1 -0
- package/dist/widgets-v2/spread.js +72 -0
- package/dist/widgets-v2/spread.js.map +1 -0
- package/dist/widgets-v2/stores.js +42 -0
- package/dist/widgets-v2/stores.js.map +1 -0
- package/dist/widgets-v2/table.js +78 -0
- package/dist/widgets-v2/table.js.map +1 -0
- package/dist/widgets-v2/timeseries.js +352 -0
- package/dist/widgets-v2/timeseries.js.map +1 -0
- package/dist/widgets-v2/utils.js +7 -0
- package/dist/widgets-v2/utils.js.map +1 -0
- package/dist/widgets-v2.js +953 -0
- package/dist/widgets-v2.js.map +1 -0
- package/package.json +73 -5
- package/src/components/lasso-tool/chip.test.tsx +176 -0
- package/src/components/lasso-tool/lasso-tool-inline.test.tsx +171 -0
- package/src/components/lasso-tool/lasso-tool.test.tsx +198 -0
- package/src/components/list-data/list-data.test.tsx +73 -0
- package/src/components/no-data-alert/no-data-alert.test.tsx +38 -0
- package/src/components/responsive-drawer/responsive-drawer.test.tsx +68 -0
- package/src/widgets/actions/brush-toggle/brush-overlay.test.tsx +465 -0
- package/src/widgets/actions/brush-toggle/brush-overlay.tsx +24 -2
- package/src/widgets/actions/brush-toggle/brush-toggle.test.tsx +208 -0
- package/src/widgets/actions/change-column/change-column-dnd.test.tsx +193 -0
- package/src/widgets/actions/change-column/sortable-column-item.test.tsx +124 -0
- package/src/widgets/actions/zoom-toggle/zoom-toggle.test.tsx +322 -0
- package/src/widgets/category/components/category-rows.test.tsx +213 -0
- package/src/widgets/echart/utils.test.ts +277 -0
- package/src/widgets/formula/config.test.ts +37 -0
- package/src/widgets/histogram/config.ts +1 -3
- package/src/widgets/range/components/range-item.test.tsx +243 -0
- package/src/widgets/stores/widget-store-branches.test.ts +275 -0
- package/src/widgets/table/config.test.ts +65 -0
- package/src/widgets/utils/chart-config/option-builders.test.ts +188 -0
- package/src/widgets-v2/PERFORMANCE.md +189 -0
- package/src/widgets-v2/actions/brush-toggle/brush-toggle.test.tsx +180 -0
- package/src/widgets-v2/actions/brush-toggle/brush-toggle.tsx +154 -0
- package/src/widgets-v2/actions/brush-toggle/index.ts +3 -0
- package/src/widgets-v2/actions/brush-toggle/labels.ts +9 -0
- package/src/widgets-v2/actions/brush-toggle/style.ts +11 -0
- package/src/widgets-v2/actions/brush-toggle/transforms.test.ts +47 -0
- package/src/widgets-v2/actions/brush-toggle/transforms.ts +31 -0
- package/src/widgets-v2/actions/change-column/change-column-icon.tsx +14 -0
- package/src/widgets-v2/actions/change-column/change-column.test.tsx +59 -0
- package/src/widgets-v2/actions/change-column/change-column.tsx +180 -0
- package/src/widgets-v2/actions/change-column/index.ts +7 -0
- package/src/widgets-v2/actions/change-column/labels.ts +9 -0
- package/src/widgets-v2/actions/change-column/sortable-column-item.tsx +56 -0
- package/src/widgets-v2/actions/change-column/style.ts +32 -0
- package/src/widgets-v2/actions/change-column/types.ts +11 -0
- package/src/widgets-v2/actions/download/download.test.tsx +327 -0
- package/src/widgets-v2/actions/download/download.tsx +144 -0
- package/src/widgets-v2/actions/download/exports.test.tsx +198 -0
- package/src/widgets-v2/actions/download/exports.ts +115 -0
- package/src/widgets-v2/actions/download/icons.tsx +26 -0
- package/src/widgets-v2/actions/download/index.ts +13 -0
- package/src/widgets-v2/actions/download/labels.ts +16 -0
- package/src/widgets-v2/actions/download/png-item.test.tsx +72 -0
- package/src/widgets-v2/actions/download/png-item.tsx +52 -0
- package/src/widgets-v2/actions/download/style.ts +3 -0
- package/src/widgets-v2/actions/download/types.ts +32 -0
- package/src/widgets-v2/actions/fullscreen/fullscreen.test.tsx +150 -0
- package/src/widgets-v2/actions/fullscreen/fullscreen.tsx +230 -0
- package/src/widgets-v2/actions/fullscreen/index.ts +7 -0
- package/src/widgets-v2/actions/fullscreen/labels.ts +9 -0
- package/src/widgets-v2/actions/fullscreen/style.ts +59 -0
- package/src/widgets-v2/actions/fullscreen/types.ts +15 -0
- package/src/widgets-v2/actions/index.ts +82 -0
- package/src/widgets-v2/actions/lock-selection/index.ts +10 -0
- package/src/widgets-v2/actions/lock-selection/labels.ts +11 -0
- package/src/widgets-v2/actions/lock-selection/lock-selection.test.tsx +187 -0
- package/src/widgets-v2/actions/lock-selection/lock-selection.tsx +130 -0
- package/src/widgets-v2/actions/lock-selection/style.ts +11 -0
- package/src/widgets-v2/actions/lock-selection/transforms.ts +27 -0
- package/src/widgets-v2/actions/relative-data/index.ts +3 -0
- package/src/widgets-v2/actions/relative-data/labels.ts +9 -0
- package/src/widgets-v2/actions/relative-data/relative-data.test.tsx +71 -0
- package/src/widgets-v2/actions/relative-data/relative-data.tsx +107 -0
- package/src/widgets-v2/actions/relative-data/style.ts +11 -0
- package/src/widgets-v2/actions/relative-data/transforms.test.ts +151 -0
- package/src/widgets-v2/actions/relative-data/transforms.ts +70 -0
- package/src/widgets-v2/actions/searcher/filter.ts +28 -0
- package/src/widgets-v2/actions/searcher/index.ts +8 -0
- package/src/widgets-v2/actions/searcher/labels.ts +13 -0
- package/src/widgets-v2/actions/searcher/searcher-toggle.tsx +91 -0
- package/src/widgets-v2/actions/searcher/searcher.test.tsx +92 -0
- package/src/widgets-v2/actions/searcher/searcher.tsx +112 -0
- package/src/widgets-v2/actions/searcher/style.ts +15 -0
- package/src/widgets-v2/actions/stack-toggle/index.ts +3 -0
- package/src/widgets-v2/actions/stack-toggle/labels.ts +9 -0
- package/src/widgets-v2/actions/stack-toggle/stack-toggle.test.tsx +61 -0
- package/src/widgets-v2/actions/stack-toggle/stack-toggle.tsx +54 -0
- package/src/widgets-v2/actions/stack-toggle/style.ts +11 -0
- package/src/widgets-v2/actions/stack-toggle/transforms.test.ts +43 -0
- package/src/widgets-v2/actions/stack-toggle/transforms.ts +25 -0
- package/src/widgets-v2/actions/zoom-toggle/index.ts +9 -0
- package/src/widgets-v2/actions/zoom-toggle/labels.ts +9 -0
- package/src/widgets-v2/actions/zoom-toggle/style.ts +11 -0
- package/src/widgets-v2/actions/zoom-toggle/transforms.test.ts +148 -0
- package/src/widgets-v2/actions/zoom-toggle/transforms.ts +171 -0
- package/src/widgets-v2/actions/zoom-toggle/zoom-toggle.test.tsx +107 -0
- package/src/widgets-v2/actions/zoom-toggle/zoom-toggle.tsx +106 -0
- package/src/widgets-v2/bar/download.test.tsx +91 -0
- package/src/widgets-v2/bar/download.tsx +66 -0
- package/src/widgets-v2/bar/index.ts +10 -0
- package/src/widgets-v2/bar/options.test.ts +317 -0
- package/src/widgets-v2/bar/options.ts +326 -0
- package/src/widgets-v2/bar/skeleton.test.tsx +19 -0
- package/src/widgets-v2/bar/skeleton.tsx +69 -0
- package/src/widgets-v2/bar/types.ts +46 -0
- package/src/widgets-v2/category/category-ui.test.tsx +746 -0
- package/src/widgets-v2/category/category-ui.tsx +389 -0
- package/src/widgets-v2/category/category.relative-data.test.tsx +107 -0
- package/src/widgets-v2/category/category.stack-toggle.test.tsx +85 -0
- package/src/widgets-v2/category/category.test.tsx +305 -0
- package/src/widgets-v2/category/category.tsx +121 -0
- package/src/widgets-v2/category/components/category-bar-stacked.test.tsx +121 -0
- package/src/widgets-v2/category/components/category-bar-stacked.tsx +73 -0
- package/src/widgets-v2/category/components/category-bar.test.tsx +64 -0
- package/src/widgets-v2/category/components/category-bar.tsx +49 -0
- package/src/widgets-v2/category/components/category-legend.test.tsx +51 -0
- package/src/widgets-v2/category/components/category-legend.tsx +39 -0
- package/src/widgets-v2/category/components/category-row-multi.tsx +86 -0
- package/src/widgets-v2/category/components/category-row-other.test.tsx +28 -0
- package/src/widgets-v2/category/components/category-row-other.tsx +33 -0
- package/src/widgets-v2/category/components/category-row-single.tsx +76 -0
- package/src/widgets-v2/category/components/category-row-stacked.test.tsx +244 -0
- package/src/widgets-v2/category/components/category-row-stacked.tsx +99 -0
- package/src/widgets-v2/category/download.test.ts +71 -0
- package/src/widgets-v2/category/download.ts +54 -0
- package/src/widgets-v2/category/index.ts +32 -0
- package/src/widgets-v2/category/skeleton.test.tsx +26 -0
- package/src/widgets-v2/category/skeleton.tsx +74 -0
- package/src/widgets-v2/category/style.ts +290 -0
- package/src/widgets-v2/category/types.ts +54 -0
- package/src/widgets-v2/echart/echart-ui.test.tsx +232 -0
- package/src/widgets-v2/echart/echart-ui.tsx +184 -0
- package/src/widgets-v2/echart/echart.test.tsx +229 -0
- package/src/widgets-v2/echart/echart.tsx +199 -0
- package/src/widgets-v2/echart/index.ts +22 -0
- package/src/widgets-v2/echart/shared-resize-observer.test.ts +91 -0
- package/src/widgets-v2/echart/shared-resize-observer.ts +56 -0
- package/src/widgets-v2/echart/style.ts +8 -0
- package/src/widgets-v2/echart/use-chart-selection.test.tsx +118 -0
- package/src/widgets-v2/echart/use-chart-selection.ts +115 -0
- package/src/widgets-v2/formula/delta.tsx +61 -0
- package/src/widgets-v2/formula/download.test.tsx +65 -0
- package/src/widgets-v2/formula/download.tsx +69 -0
- package/src/widgets-v2/formula/formula-ui.test.tsx +91 -0
- package/src/widgets-v2/formula/formula-ui.tsx +66 -0
- package/src/widgets-v2/formula/formula.test.tsx +50 -0
- package/src/widgets-v2/formula/formula.tsx +34 -0
- package/src/widgets-v2/formula/index.ts +17 -0
- package/src/widgets-v2/formula/note.tsx +25 -0
- package/src/widgets-v2/formula/prefix.tsx +25 -0
- package/src/widgets-v2/formula/series.tsx +67 -0
- package/src/widgets-v2/formula/skeleton.test.tsx +21 -0
- package/src/widgets-v2/formula/skeleton.tsx +27 -0
- package/src/widgets-v2/formula/style.ts +31 -0
- package/src/widgets-v2/formula/subcomponents.test.tsx +107 -0
- package/src/widgets-v2/formula/suffix.tsx +25 -0
- package/src/widgets-v2/formula/types.ts +44 -0
- package/src/widgets-v2/formula/value.tsx +31 -0
- package/src/widgets-v2/histogram/download.test.ts +94 -0
- package/src/widgets-v2/histogram/download.ts +60 -0
- package/src/widgets-v2/histogram/index.ts +10 -0
- package/src/widgets-v2/histogram/options.test.ts +304 -0
- package/src/widgets-v2/histogram/options.ts +337 -0
- package/src/widgets-v2/histogram/skeleton.test.tsx +16 -0
- package/src/widgets-v2/histogram/skeleton.tsx +70 -0
- package/src/widgets-v2/histogram/transforms.test.ts +46 -0
- package/src/widgets-v2/histogram/transforms.ts +30 -0
- package/src/widgets-v2/histogram/types.ts +51 -0
- package/src/widgets-v2/index.ts +201 -0
- package/src/widgets-v2/markdown/download.test.ts +66 -0
- package/src/widgets-v2/markdown/download.ts +53 -0
- package/src/widgets-v2/markdown/index.ts +6 -0
- package/src/widgets-v2/markdown/markdown-content.test.tsx +155 -0
- package/src/widgets-v2/markdown/markdown-content.tsx +72 -0
- package/src/widgets-v2/markdown/markdown-ui.test.tsx +75 -0
- package/src/widgets-v2/markdown/markdown-ui.tsx +55 -0
- package/src/widgets-v2/markdown/markdown.test.tsx +39 -0
- package/src/widgets-v2/markdown/markdown.tsx +17 -0
- package/src/widgets-v2/markdown/skeleton.test.tsx +15 -0
- package/src/widgets-v2/markdown/skeleton.tsx +32 -0
- package/src/widgets-v2/markdown/style.ts +53 -0
- package/src/widgets-v2/markdown/types.ts +4 -0
- package/src/widgets-v2/note/labels.ts +9 -0
- package/src/widgets-v2/note/style.ts +26 -0
- package/src/widgets-v2/note/widget-note.test.tsx +158 -0
- package/src/widgets-v2/note/widget-note.tsx +172 -0
- package/src/widgets-v2/pie/download.test.ts +78 -0
- package/src/widgets-v2/pie/download.ts +55 -0
- package/src/widgets-v2/pie/index.ts +10 -0
- package/src/widgets-v2/pie/options.test.ts +585 -0
- package/src/widgets-v2/pie/options.ts +509 -0
- package/src/widgets-v2/pie/skeleton.test.tsx +17 -0
- package/src/widgets-v2/pie/skeleton.tsx +32 -0
- package/src/widgets-v2/pie/types.ts +55 -0
- package/src/widgets-v2/provider/widget-provider.test.tsx +119 -0
- package/src/widgets-v2/provider/widget-provider.tsx +111 -0
- package/src/widgets-v2/range/index.ts +4 -0
- package/src/widgets-v2/range/range-ui.test.tsx +130 -0
- package/src/widgets-v2/range/range-ui.tsx +211 -0
- package/src/widgets-v2/range/range.test.tsx +68 -0
- package/src/widgets-v2/range/range.tsx +46 -0
- package/src/widgets-v2/range/skeleton.test.tsx +17 -0
- package/src/widgets-v2/range/skeleton.tsx +47 -0
- package/src/widgets-v2/range/style.ts +41 -0
- package/src/widgets-v2/range/types.ts +37 -0
- package/src/widgets-v2/scatterplot/download.test.ts +71 -0
- package/src/widgets-v2/scatterplot/download.ts +54 -0
- package/src/widgets-v2/scatterplot/index.ts +11 -0
- package/src/widgets-v2/scatterplot/options.test.ts +399 -0
- package/src/widgets-v2/scatterplot/options.ts +421 -0
- package/src/widgets-v2/scatterplot/skeleton.test.tsx +17 -0
- package/src/widgets-v2/scatterplot/skeleton.tsx +84 -0
- package/src/widgets-v2/scatterplot/transforms.test.ts +97 -0
- package/src/widgets-v2/scatterplot/transforms.ts +38 -0
- package/src/widgets-v2/scatterplot/types.ts +55 -0
- package/src/widgets-v2/selection-summary/labels.ts +11 -0
- package/src/widgets-v2/selection-summary/selection-summary.test.tsx +53 -0
- package/src/widgets-v2/selection-summary/selection-summary.tsx +62 -0
- package/src/widgets-v2/selection-summary/style.ts +23 -0
- package/src/widgets-v2/spread/download.test.ts +64 -0
- package/src/widgets-v2/spread/download.ts +59 -0
- package/src/widgets-v2/spread/index.ts +6 -0
- package/src/widgets-v2/spread/separator.tsx +11 -0
- package/src/widgets-v2/spread/skeleton.test.tsx +17 -0
- package/src/widgets-v2/spread/skeleton.tsx +38 -0
- package/src/widgets-v2/spread/spread-ui.test.tsx +108 -0
- package/src/widgets-v2/spread/spread-ui.tsx +52 -0
- package/src/widgets-v2/spread/spread.test.tsx +50 -0
- package/src/widgets-v2/spread/spread.tsx +31 -0
- package/src/widgets-v2/spread/types.ts +27 -0
- package/src/widgets-v2/state/labels.test.ts +33 -0
- package/src/widgets-v2/state/labels.ts +20 -0
- package/src/widgets-v2/state/style.ts +25 -0
- package/src/widgets-v2/state/widget-state.test.tsx +294 -0
- package/src/widgets-v2/state/widget-state.tsx +184 -0
- package/src/widgets-v2/stores/index.ts +49 -0
- package/src/widgets-v2/stores/pipeline-middleware.test.ts +187 -0
- package/src/widgets-v2/stores/pipeline-middleware.ts +91 -0
- package/src/widgets-v2/stores/transforms.test.ts +162 -0
- package/src/widgets-v2/stores/transforms.ts +70 -0
- package/src/widgets-v2/stores/types.ts +64 -0
- package/src/widgets-v2/stores/use-echart-instance.test.tsx +91 -0
- package/src/widgets-v2/stores/use-echart-instance.ts +29 -0
- package/src/widgets-v2/stores/use-transform-enabled.test.tsx +127 -0
- package/src/widgets-v2/stores/use-transform-enabled.ts +25 -0
- package/src/widgets-v2/stores/use-transform.test.tsx +262 -0
- package/src/widgets-v2/stores/use-transform.ts +158 -0
- package/src/widgets-v2/stores/widget-context.test.tsx +58 -0
- package/src/widgets-v2/stores/widget-context.ts +15 -0
- package/src/widgets-v2/stores/widget-store-registry.test.ts +292 -0
- package/src/widgets-v2/stores/widget-store-registry.ts +248 -0
- package/src/widgets-v2/subheader/style.ts +12 -0
- package/src/widgets-v2/subheader/subheader.test.tsx +30 -0
- package/src/widgets-v2/subheader/subheader.tsx +16 -0
- package/src/widgets-v2/table/download.test.ts +75 -0
- package/src/widgets-v2/table/download.ts +47 -0
- package/src/widgets-v2/table/helpers.test.ts +214 -0
- package/src/widgets-v2/table/helpers.ts +136 -0
- package/src/widgets-v2/table/index.ts +23 -0
- package/src/widgets-v2/table/labels.tsx +41 -0
- package/src/widgets-v2/table/skeleton.test.tsx +26 -0
- package/src/widgets-v2/table/skeleton.tsx +65 -0
- package/src/widgets-v2/table/style.ts +46 -0
- package/src/widgets-v2/table/table-ui.test.tsx +200 -0
- package/src/widgets-v2/table/table-ui.tsx +331 -0
- package/src/widgets-v2/table/table.test.tsx +119 -0
- package/src/widgets-v2/table/table.tsx +174 -0
- package/src/widgets-v2/table/types.ts +44 -0
- package/src/widgets-v2/test-utils.ts +107 -0
- package/src/widgets-v2/timeseries/download.test.ts +95 -0
- package/src/widgets-v2/timeseries/download.ts +86 -0
- package/src/widgets-v2/timeseries/index.ts +10 -0
- package/src/widgets-v2/timeseries/options.test.ts +379 -0
- package/src/widgets-v2/timeseries/options.ts +341 -0
- package/src/widgets-v2/timeseries/skeleton.test.tsx +13 -0
- package/src/widgets-v2/timeseries/skeleton.tsx +76 -0
- package/src/widgets-v2/timeseries/types.ts +61 -0
- package/src/widgets-v2/toolbox/labels.ts +9 -0
- package/src/widgets-v2/toolbox/style.ts +33 -0
- package/src/widgets-v2/toolbox/toolbox.test.tsx +200 -0
- package/src/widgets-v2/toolbox/toolbox.tsx +309 -0
- package/src/widgets-v2/utils/data-zoom-layout.ts +26 -0
- package/src/widgets-v2/utils/index.ts +2 -0
- package/src/widgets-v2/utils/merge-options.test.ts +52 -0
- package/src/widgets-v2/utils/merge-options.ts +50 -0
- package/src/widgets-v2/wrapper/index.ts +14 -0
- package/src/widgets-v2/wrapper/labels.ts +11 -0
- package/src/widgets-v2/wrapper/style.ts +134 -0
- package/src/widgets-v2/wrapper/widget-actions.test.tsx +52 -0
- package/src/widgets-v2/wrapper/widget-actions.tsx +43 -0
- package/src/widgets-v2/wrapper/widget-content.test.tsx +27 -0
- package/src/widgets-v2/wrapper/widget-content.tsx +29 -0
- package/src/widgets-v2/wrapper/widget-wrapper.test.tsx +159 -0
- package/src/widgets-v2/wrapper/widget-wrapper.tsx +178 -0
- package/dist/styles-BYTyKQFP.js.map +0 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { useCallback, useState, type ComponentType } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
CircularProgress,
|
|
4
|
+
IconButton,
|
|
5
|
+
ListItemIcon,
|
|
6
|
+
ListItemText,
|
|
7
|
+
Menu,
|
|
8
|
+
MenuItem,
|
|
9
|
+
type SvgIconProps,
|
|
10
|
+
} from '@mui/material'
|
|
11
|
+
import DownloadIcon from '@mui/icons-material/FileDownload'
|
|
12
|
+
import { Tooltip } from '../../../components'
|
|
13
|
+
import { triggerLinkDownload } from './exports'
|
|
14
|
+
import type { DownloadItem } from './types'
|
|
15
|
+
import { DEFAULT_DOWNLOAD_LABELS, type DownloadLabels } from './labels'
|
|
16
|
+
|
|
17
|
+
export interface DownloadProps {
|
|
18
|
+
items: readonly DownloadItem[]
|
|
19
|
+
labels?: Partial<DownloadLabels>
|
|
20
|
+
icon?: ComponentType<SvgIconProps>
|
|
21
|
+
iconProps?: SvgIconProps
|
|
22
|
+
/**
|
|
23
|
+
* Fires when an item's `resolve()` rejects (or any step in the click-through
|
|
24
|
+
* path throws). The error is normalised to a real `Error` first. Surface
|
|
25
|
+
* to your telemetry / toast layer here; the trigger already shows a visual
|
|
26
|
+
* error indicator independently.
|
|
27
|
+
*/
|
|
28
|
+
onError?: (err: Error) => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function Download({
|
|
32
|
+
items,
|
|
33
|
+
labels,
|
|
34
|
+
icon: Icon = DownloadIcon,
|
|
35
|
+
iconProps,
|
|
36
|
+
onError,
|
|
37
|
+
}: DownloadProps) {
|
|
38
|
+
const _labels = { ...DEFAULT_DOWNLOAD_LABELS, ...labels }
|
|
39
|
+
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)
|
|
40
|
+
const [isDownloading, setIsDownloading] = useState(false)
|
|
41
|
+
const [error, setError] = useState<Error | null>(null)
|
|
42
|
+
|
|
43
|
+
const open = useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
|
|
44
|
+
// Clear any prior error when the user re-engages the menu — the next
|
|
45
|
+
// attempt is a fresh interaction, not a continuation of the failed one.
|
|
46
|
+
setError(null)
|
|
47
|
+
setAnchorEl(e.currentTarget)
|
|
48
|
+
}, [])
|
|
49
|
+
const close = useCallback(() => setAnchorEl(null), [])
|
|
50
|
+
|
|
51
|
+
const onSelect = useCallback(
|
|
52
|
+
(item: DownloadItem) => {
|
|
53
|
+
// Single-flight: ignore re-entry while a download is already in flight.
|
|
54
|
+
// The trigger is `disabled` and the menu is closed during that window,
|
|
55
|
+
// so this guard mostly defends against future call sites that bypass
|
|
56
|
+
// the menu (e.g. keyboard shortcuts).
|
|
57
|
+
if (item.disabled || isDownloading) return
|
|
58
|
+
close()
|
|
59
|
+
setIsDownloading(true)
|
|
60
|
+
setError(null)
|
|
61
|
+
item
|
|
62
|
+
.resolve()
|
|
63
|
+
.then(({ url, filename, revoke }) => {
|
|
64
|
+
// `item.filename` overrides what `resolve()` returns — lets consumers
|
|
65
|
+
// template the filename with widget metadata declaratively.
|
|
66
|
+
triggerLinkDownload({ url, filename: item.filename ?? filename })
|
|
67
|
+
// Defer revoke past the current task so Safari / Firefox-on-slow-disk
|
|
68
|
+
// can dispatch the download before the blob URL is invalidated.
|
|
69
|
+
// Synchronous revoke here can silently cancel the download.
|
|
70
|
+
if (revoke) setTimeout(revoke, 0)
|
|
71
|
+
})
|
|
72
|
+
.catch((err: unknown) => {
|
|
73
|
+
const e = err instanceof Error ? err : new Error(String(err))
|
|
74
|
+
setError(e)
|
|
75
|
+
onError?.(e)
|
|
76
|
+
})
|
|
77
|
+
.finally(() => {
|
|
78
|
+
setIsDownloading(false)
|
|
79
|
+
})
|
|
80
|
+
},
|
|
81
|
+
[close, isDownloading, onError],
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
const triggerLabel = isDownloading
|
|
85
|
+
? _labels.loading
|
|
86
|
+
: error
|
|
87
|
+
? _labels.error
|
|
88
|
+
: _labels.trigger
|
|
89
|
+
const triggerActive = Boolean(anchorEl) || isDownloading
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<>
|
|
93
|
+
<Tooltip title={triggerLabel}>
|
|
94
|
+
<IconButton
|
|
95
|
+
size='small'
|
|
96
|
+
aria-label={triggerLabel}
|
|
97
|
+
aria-haspopup='true'
|
|
98
|
+
aria-expanded={anchorEl ? 'true' : undefined}
|
|
99
|
+
aria-busy={isDownloading || undefined}
|
|
100
|
+
data-error={error ? 'true' : undefined}
|
|
101
|
+
disabled={isDownloading || items.length === 0}
|
|
102
|
+
onClick={open}
|
|
103
|
+
className={triggerActive ? 'active' : undefined}
|
|
104
|
+
>
|
|
105
|
+
{isDownloading ? (
|
|
106
|
+
<CircularProgress size={18} color='inherit' />
|
|
107
|
+
) : (
|
|
108
|
+
<Icon fontSize='small' {...iconProps} />
|
|
109
|
+
)}
|
|
110
|
+
</IconButton>
|
|
111
|
+
</Tooltip>
|
|
112
|
+
<Menu
|
|
113
|
+
anchorEl={anchorEl}
|
|
114
|
+
open={Boolean(anchorEl)}
|
|
115
|
+
onClose={close}
|
|
116
|
+
variant='menu'
|
|
117
|
+
elevation={8}
|
|
118
|
+
anchorOrigin={{
|
|
119
|
+
vertical: 'bottom',
|
|
120
|
+
horizontal: 'right',
|
|
121
|
+
}}
|
|
122
|
+
transformOrigin={{
|
|
123
|
+
vertical: 'top',
|
|
124
|
+
horizontal: 'right',
|
|
125
|
+
}}
|
|
126
|
+
>
|
|
127
|
+
{items.map((item) => (
|
|
128
|
+
<MenuItem
|
|
129
|
+
key={item.id}
|
|
130
|
+
disabled={item.disabled}
|
|
131
|
+
onClick={() => void onSelect(item)}
|
|
132
|
+
>
|
|
133
|
+
{item.icon && (
|
|
134
|
+
<ListItemIcon sx={{ color: 'text.primary' }}>
|
|
135
|
+
{item.icon}
|
|
136
|
+
</ListItemIcon>
|
|
137
|
+
)}
|
|
138
|
+
<ListItemText>{item.label}</ListItemText>
|
|
139
|
+
</MenuItem>
|
|
140
|
+
))}
|
|
141
|
+
</Menu>
|
|
142
|
+
</>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
|
+
import { render } from '@testing-library/react'
|
|
3
|
+
|
|
4
|
+
// Hoisted mock for html2canvas — tests reach into `mockToBlob` to switch
|
|
5
|
+
// between "blob produced" and "toBlob returned null" without re-mocking.
|
|
6
|
+
type ToBlobCb = (blob: Blob | null) => void
|
|
7
|
+
const mockToBlob = vi.fn((cb: ToBlobCb) => cb(new Blob(['png-bytes'])))
|
|
8
|
+
|
|
9
|
+
vi.mock('html2canvas', () => ({
|
|
10
|
+
default: () => Promise.resolve({ toBlob: mockToBlob }),
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
toCsvString,
|
|
15
|
+
downloadToCSV,
|
|
16
|
+
triggerLinkDownload,
|
|
17
|
+
downloadDOMToPNG,
|
|
18
|
+
sanitizeFilename,
|
|
19
|
+
} from './exports'
|
|
20
|
+
import { CSVIcon, PNGIcon } from './icons'
|
|
21
|
+
|
|
22
|
+
let csvText = ''
|
|
23
|
+
|
|
24
|
+
let revokeSpy: ReturnType<typeof vi.spyOn>
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
csvText = ''
|
|
28
|
+
mockToBlob.mockReset()
|
|
29
|
+
mockToBlob.mockImplementation((cb: ToBlobCb) => cb(new Blob(['png-bytes'])))
|
|
30
|
+
vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:mock')
|
|
31
|
+
revokeSpy = vi
|
|
32
|
+
.spyOn(URL, 'revokeObjectURL')
|
|
33
|
+
.mockImplementation(() => undefined)
|
|
34
|
+
const RealBlob = global.Blob
|
|
35
|
+
vi.stubGlobal(
|
|
36
|
+
'Blob',
|
|
37
|
+
class extends RealBlob {
|
|
38
|
+
constructor(parts: BlobPart[], opts?: BlobPropertyBag) {
|
|
39
|
+
csvText = typeof parts[0] === 'string' ? parts[0] : ''
|
|
40
|
+
super(parts, opts)
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('toCsvString', () => {
|
|
47
|
+
it('joins rows with newlines and cells with commas', () => {
|
|
48
|
+
expect(
|
|
49
|
+
toCsvString([
|
|
50
|
+
['name', 'value'],
|
|
51
|
+
['a', 1],
|
|
52
|
+
['b', 2],
|
|
53
|
+
]),
|
|
54
|
+
).toBe('name,value\na,1\nb,2')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('emits blank cells for null and undefined', () => {
|
|
58
|
+
expect(toCsvString([[null, undefined, 'x']])).toBe(',,x')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('stringifies numbers and booleans', () => {
|
|
62
|
+
expect(toCsvString([[1, false, true]])).toBe('1,false,true')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('JSON.stringifies objects and arrays', () => {
|
|
66
|
+
expect(toCsvString([[{ a: 1 }, [1, 2]]])).toBe('"{""a"":1}","[1,2]"')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('quotes cells containing comma / quote / newline', () => {
|
|
70
|
+
expect(toCsvString([['a,b', 'he said "hi"', 'line1\nline2']])).toBe(
|
|
71
|
+
'"a,b","he said ""hi""","line1\nline2"',
|
|
72
|
+
)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe('formula injection guard', () => {
|
|
76
|
+
it.each([
|
|
77
|
+
[
|
|
78
|
+
'=',
|
|
79
|
+
'=HYPERLINK("https://attacker.example")',
|
|
80
|
+
'\'=HYPERLINK("https://attacker.example")',
|
|
81
|
+
],
|
|
82
|
+
['+', '+1+1', "'+1+1"],
|
|
83
|
+
['-', '-2+1', "'-2+1"],
|
|
84
|
+
['@', '@SUM(A1:A2)', "'@SUM(A1:A2)"],
|
|
85
|
+
['\\t', '\tleading-tab', "'\tleading-tab"],
|
|
86
|
+
['\\r', '\rleading-cr', "'\rleading-cr"],
|
|
87
|
+
])(
|
|
88
|
+
'prefixes cells starting with %s with a single quote',
|
|
89
|
+
(_label, raw, expectedRaw) => {
|
|
90
|
+
// The escape step also quotes if the value contains comma/quote/newline,
|
|
91
|
+
// so we re-derive the exact wire representation from the raw expected.
|
|
92
|
+
const expected = /[",\n\r]/.test(expectedRaw)
|
|
93
|
+
? `"${expectedRaw.replace(/"/g, '""')}"`
|
|
94
|
+
: expectedRaw
|
|
95
|
+
expect(toCsvString([[raw]])).toBe(expected)
|
|
96
|
+
},
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
it('does not touch values where the dangerous char is not leading', () => {
|
|
100
|
+
expect(toCsvString([['a=b']])).toBe('a=b')
|
|
101
|
+
expect(toCsvString([['1+2']])).toBe('1+2')
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
describe('downloadToCSV', () => {
|
|
107
|
+
it('serialises rows then wraps them in a URL handle with a working revoke', () => {
|
|
108
|
+
const handle = downloadToCSV([['x', 1]])
|
|
109
|
+
expect(handle.url).toBe('blob:mock')
|
|
110
|
+
expect(csvText).toBe('x,1')
|
|
111
|
+
handle.revoke?.()
|
|
112
|
+
expect(revokeSpy).toHaveBeenCalledWith('blob:mock')
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
describe('triggerLinkDownload', () => {
|
|
117
|
+
it('synthesises an anchor, clicks it, then removes it', () => {
|
|
118
|
+
const appendSpy = vi.spyOn(document.body, 'appendChild')
|
|
119
|
+
triggerLinkDownload({ url: 'blob:x', filename: 'demo.csv' })
|
|
120
|
+
expect(appendSpy).toHaveBeenCalled()
|
|
121
|
+
const anchor = appendSpy.mock.calls[0]?.[0] as HTMLAnchorElement
|
|
122
|
+
expect(anchor.href).toBe('blob:x')
|
|
123
|
+
expect(anchor.download).toBe('demo.csv')
|
|
124
|
+
expect(anchor.isConnected).toBe(false)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('applies sanitizeFilename to the anchor download attribute', () => {
|
|
128
|
+
const appendSpy = vi.spyOn(document.body, 'appendChild')
|
|
129
|
+
const before = appendSpy.mock.calls.length
|
|
130
|
+
triggerLinkDownload({
|
|
131
|
+
url: 'blob:y',
|
|
132
|
+
filename: '..\\evil\r\nname.csv',
|
|
133
|
+
})
|
|
134
|
+
const anchor = appendSpy.mock.calls[before]?.[0] as HTMLAnchorElement
|
|
135
|
+
expect(anchor.download).toBe('.._evil__name.csv')
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
describe('sanitizeFilename', () => {
|
|
140
|
+
it('replaces path separators, NUL, and ASCII control chars with underscore', () => {
|
|
141
|
+
expect(sanitizeFilename('a/b\\c\x00\x07d\r\ne.csv')).toBe('a_b_c__d__e.csv')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('clamps long filenames to 200 chars', () => {
|
|
145
|
+
const long = 'x'.repeat(500) + '.csv'
|
|
146
|
+
const out = sanitizeFilename(long)
|
|
147
|
+
expect(out.length).toBe(200)
|
|
148
|
+
expect(out.startsWith('x')).toBe(true)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('falls back to "download" for empty or whitespace-only input', () => {
|
|
152
|
+
expect(sanitizeFilename('')).toBe('download')
|
|
153
|
+
// Whitespace stays — sanitizer only handles control bytes — but a leading
|
|
154
|
+
// NUL run that empties the string still falls back.
|
|
155
|
+
expect(sanitizeFilename('\x00\x00\x00')).toBe('___')
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
describe('downloadDOMToPNG', () => {
|
|
160
|
+
it('rasterises the element via html2canvas and resolves with a handle', async () => {
|
|
161
|
+
const handle = await downloadDOMToPNG({
|
|
162
|
+
element: document.createElement('div'),
|
|
163
|
+
})
|
|
164
|
+
expect(handle.url).toBe('blob:mock')
|
|
165
|
+
handle.revoke?.()
|
|
166
|
+
expect(revokeSpy).toHaveBeenCalledWith('blob:mock')
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('rejects when canvas.toBlob returns null', async () => {
|
|
170
|
+
mockToBlob.mockImplementation((cb: ToBlobCb) => cb(null))
|
|
171
|
+
await expect(
|
|
172
|
+
downloadDOMToPNG({ element: document.createElement('div') }),
|
|
173
|
+
).rejects.toThrow(/toBlob/)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('forwards pixelRatio and backgroundColor overrides', async () => {
|
|
177
|
+
// We can't easily inspect the call args without re-hoisting the mock,
|
|
178
|
+
// so just guard the happy path still works with overrides supplied.
|
|
179
|
+
const handle = await downloadDOMToPNG({
|
|
180
|
+
element: document.createElement('div'),
|
|
181
|
+
pixelRatio: 4,
|
|
182
|
+
backgroundColor: '#fff',
|
|
183
|
+
})
|
|
184
|
+
expect(handle.url).toBe('blob:mock')
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
describe('download icons', () => {
|
|
189
|
+
it('CSVIcon renders an SVG element', () => {
|
|
190
|
+
const { container } = render(<CSVIcon fontSize='small' />)
|
|
191
|
+
expect(container.querySelector('svg')).not.toBeNull()
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('PNGIcon renders an SVG element', () => {
|
|
195
|
+
const { container } = render(<PNGIcon fontSize='small' />)
|
|
196
|
+
expect(container.querySelector('svg')).not.toBeNull()
|
|
197
|
+
})
|
|
198
|
+
})
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure download helpers — no React, no DOM lifecycle outside the link click.
|
|
3
|
+
* Component layer in {@link ./download.tsx} orchestrates these.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import html2canvas from 'html2canvas'
|
|
7
|
+
|
|
8
|
+
export interface DownloadHandle {
|
|
9
|
+
url: string
|
|
10
|
+
revoke: () => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Leading characters that Excel / Sheets / Numbers interpret as a formula
|
|
14
|
+
// when the CSV is opened. Prefixing the cell with a single quote forces the
|
|
15
|
+
// spreadsheet app to render it as plain text. Tab and CR are included
|
|
16
|
+
// because some tooling strips leading whitespace before formula evaluation.
|
|
17
|
+
const CSV_FORMULA_PREFIX = /^[=+\-@\t\r]/
|
|
18
|
+
|
|
19
|
+
export function toCsvString(rows: readonly (readonly unknown[])[]): string {
|
|
20
|
+
const escape = (cell: unknown): string => {
|
|
21
|
+
if (cell == null) return ''
|
|
22
|
+
let value =
|
|
23
|
+
typeof cell === 'string'
|
|
24
|
+
? cell
|
|
25
|
+
: typeof cell === 'number' || typeof cell === 'boolean'
|
|
26
|
+
? String(cell)
|
|
27
|
+
: JSON.stringify(cell)
|
|
28
|
+
if (CSV_FORMULA_PREFIX.test(value)) value = `'${value}`
|
|
29
|
+
if (/[",\n\r]/.test(value)) return `"${value.replace(/"/g, '""')}"`
|
|
30
|
+
return value
|
|
31
|
+
}
|
|
32
|
+
return rows.map((row) => row.map(escape).join(',')).join('\n')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function downloadToCSV(
|
|
36
|
+
rows: readonly (readonly unknown[])[],
|
|
37
|
+
): DownloadHandle {
|
|
38
|
+
const csv = toCsvString(rows)
|
|
39
|
+
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
|
|
40
|
+
const url = URL.createObjectURL(blob)
|
|
41
|
+
return { url, revoke: () => URL.revokeObjectURL(url) }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Path separators, NUL, and ASCII control chars. Filenames containing these
|
|
45
|
+
// can confuse the browser download stack and historically enabled
|
|
46
|
+
// Content-Disposition-style trickery.
|
|
47
|
+
// eslint-disable-next-line no-control-regex
|
|
48
|
+
const UNSAFE_FILENAME_CHARS = /[\\/\x00-\x1f\x7f]/g
|
|
49
|
+
const MAX_FILENAME_LENGTH = 200
|
|
50
|
+
|
|
51
|
+
export function sanitizeFilename(name: string): string {
|
|
52
|
+
return (
|
|
53
|
+
(name ?? '')
|
|
54
|
+
.replace(UNSAFE_FILENAME_CHARS, '_')
|
|
55
|
+
.slice(0, MAX_FILENAME_LENGTH) || 'download'
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Triggers a browser download for the given URL by synthesizing an anchor,
|
|
61
|
+
* clicking it, and removing it. Pure DOM — no React. The filename is run
|
|
62
|
+
* through {@link sanitizeFilename} before being assigned to `a.download`.
|
|
63
|
+
*/
|
|
64
|
+
export function triggerLinkDownload(args: {
|
|
65
|
+
url: string
|
|
66
|
+
filename: string
|
|
67
|
+
}): void {
|
|
68
|
+
const { url, filename } = args
|
|
69
|
+
const a = document.createElement('a')
|
|
70
|
+
a.href = url
|
|
71
|
+
a.download = sanitizeFilename(filename)
|
|
72
|
+
a.style.display = 'none'
|
|
73
|
+
document.body.appendChild(a)
|
|
74
|
+
a.click()
|
|
75
|
+
a.remove()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface DownloadDOMToPNGOptions {
|
|
79
|
+
element: HTMLElement
|
|
80
|
+
/** html2canvas `scale`. Default 2 — crisp on hi-DPI displays. */
|
|
81
|
+
pixelRatio?: number
|
|
82
|
+
/**
|
|
83
|
+
* html2canvas `backgroundColor`. Default `null` (transparent) so the PNG
|
|
84
|
+
* inherits whatever surface it lands on when the user pastes it.
|
|
85
|
+
*/
|
|
86
|
+
backgroundColor?: string | null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Rasterise an HTMLElement to PNG via `html2canvas` and wrap the result in
|
|
91
|
+
* a `DownloadHandle`. The caller is responsible for calling `handle.revoke`
|
|
92
|
+
* after the link click is dispatched. Used by per-widget download configs
|
|
93
|
+
* to power the PNG menu item.
|
|
94
|
+
*/
|
|
95
|
+
export async function downloadDOMToPNG(
|
|
96
|
+
opts: DownloadDOMToPNGOptions,
|
|
97
|
+
): Promise<DownloadHandle> {
|
|
98
|
+
const { element, pixelRatio = 2, backgroundColor = null } = opts
|
|
99
|
+
const canvas = await html2canvas(element, {
|
|
100
|
+
scale: pixelRatio,
|
|
101
|
+
backgroundColor,
|
|
102
|
+
useCORS: true,
|
|
103
|
+
logging: false,
|
|
104
|
+
})
|
|
105
|
+
return new Promise<DownloadHandle>((resolve, reject) => {
|
|
106
|
+
canvas.toBlob((blob) => {
|
|
107
|
+
if (!blob) {
|
|
108
|
+
reject(new Error('[widgets-v2] toBlob() returned null'))
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
const url = URL.createObjectURL(blob)
|
|
112
|
+
resolve({ url, revoke: () => URL.revokeObjectURL(url) })
|
|
113
|
+
}, 'image/png')
|
|
114
|
+
})
|
|
115
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { SvgIcon, type SvgIconProps } from '@mui/material'
|
|
2
|
+
import ImageOutlined from '@mui/icons-material/ImageOutlined'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generic "image" glyph used for the PNG download item. Wraps MUI's
|
|
6
|
+
* `ImageOutlined` so callers can swap it via the per-widget download
|
|
7
|
+
* config's `icon` override without pulling MUI directly.
|
|
8
|
+
*/
|
|
9
|
+
export function PNGIcon(props: SvgIconProps) {
|
|
10
|
+
return <ImageOutlined {...props} />
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* "CSV" rectangle with the letters spelled inside — matches v1 visual and is
|
|
15
|
+
* easier to recognise in a download menu than a generic table glyph.
|
|
16
|
+
*/
|
|
17
|
+
export function CSVIcon(props: SvgIconProps) {
|
|
18
|
+
return (
|
|
19
|
+
<SvgIcon viewBox='0 0 18 18' {...props}>
|
|
20
|
+
<path
|
|
21
|
+
fill='currentColor'
|
|
22
|
+
d='M4.313 11.25h2.25v-1.125H4.688v-2.25h1.875V6.75h-2.25a.726.726 0 0 0-.535.216.726.726 0 0 0-.216.534v3c0 .213.072.39.216.534a.726.726 0 0 0 .534.216Zm2.925 0h2.25c.212 0 .39-.072.534-.216a.726.726 0 0 0 .216-.534V9.375a.931.931 0 0 0-.216-.59.658.658 0 0 0-.534-.273H8.363v-.637h1.875V6.75h-2.25a.726.726 0 0 0-.535.216.726.726 0 0 0-.216.534v1.125c0 .213.072.403.216.572a.675.675 0 0 0 .534.253h1.126v.675H7.238v1.125Zm4.95 0h1.124l1.313-4.5H13.5l-.75 2.588L12 6.75h-1.125l1.313 4.5ZM3 15c-.413 0-.766-.147-1.06-.44a1.445 1.445 0 0 1-.44-1.06v-9c0-.412.147-.766.44-1.06C2.235 3.148 2.588 3 3 3h12c.412 0 .766.147 1.06.44.293.294.44.648.44 1.06v9c0 .412-.147.766-.44 1.06-.294.293-.647.44-1.06.44H3Zm0-1.5h12v-9H3v9Z'
|
|
23
|
+
/>
|
|
24
|
+
</SvgIcon>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { Download, type DownloadProps } from './download'
|
|
2
|
+
export {
|
|
3
|
+
downloadToCSV,
|
|
4
|
+
downloadDOMToPNG,
|
|
5
|
+
toCsvString,
|
|
6
|
+
triggerLinkDownload,
|
|
7
|
+
type DownloadHandle,
|
|
8
|
+
type DownloadDOMToPNGOptions,
|
|
9
|
+
} from './exports'
|
|
10
|
+
export { CSVIcon, PNGIcon } from './icons'
|
|
11
|
+
export { buildPngDownloadItem, type BuildPngDownloadItemArgs } from './png-item'
|
|
12
|
+
export type { DownloadItem } from './types'
|
|
13
|
+
export { DEFAULT_DOWNLOAD_LABELS, type DownloadLabels } from './labels'
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface DownloadLabels {
|
|
2
|
+
trigger: string
|
|
3
|
+
/** Tooltip / aria-label shown on the trigger while a download is in flight. */
|
|
4
|
+
loading: string
|
|
5
|
+
/**
|
|
6
|
+
* Tooltip / aria-label shown on the trigger after a download failed. Cleared
|
|
7
|
+
* the next time the menu opens.
|
|
8
|
+
*/
|
|
9
|
+
error: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_DOWNLOAD_LABELS: DownloadLabels = {
|
|
13
|
+
trigger: 'Download',
|
|
14
|
+
loading: 'Downloading…',
|
|
15
|
+
error: 'Download failed',
|
|
16
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
|
+
import { buildPngDownloadItem } from './png-item'
|
|
3
|
+
|
|
4
|
+
type ToBlobCb = (blob: Blob | null) => void
|
|
5
|
+
const mockToBlob = vi.fn((cb: ToBlobCb) => cb(new Blob(['png-bytes'])))
|
|
6
|
+
|
|
7
|
+
vi.mock('html2canvas', () => ({
|
|
8
|
+
default: () => Promise.resolve({ toBlob: mockToBlob }),
|
|
9
|
+
}))
|
|
10
|
+
|
|
11
|
+
let revokeSpy: ReturnType<typeof vi.spyOn>
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
mockToBlob.mockReset()
|
|
15
|
+
mockToBlob.mockImplementation((cb: ToBlobCb) => cb(new Blob(['png-bytes'])))
|
|
16
|
+
vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:png')
|
|
17
|
+
revokeSpy = vi
|
|
18
|
+
.spyOn(URL, 'revokeObjectURL')
|
|
19
|
+
.mockImplementation(() => undefined)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('buildPngDownloadItem', () => {
|
|
23
|
+
it('returns an item with the canonical PNG shape (id, label, icon)', () => {
|
|
24
|
+
const item = buildPngDownloadItem({
|
|
25
|
+
filename: 'demo',
|
|
26
|
+
getCaptureEl: () => document.createElement('div'),
|
|
27
|
+
})
|
|
28
|
+
expect(item.id).toBe('png')
|
|
29
|
+
expect(item.label).toBe('PNG')
|
|
30
|
+
expect(item.icon).toBeTruthy()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('honours a label override', () => {
|
|
34
|
+
const item = buildPngDownloadItem({
|
|
35
|
+
filename: 'demo',
|
|
36
|
+
getCaptureEl: () => document.createElement('div'),
|
|
37
|
+
label: 'Export image',
|
|
38
|
+
})
|
|
39
|
+
expect(item.label).toBe('Export image')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('resolve() rasterises the capture element and returns a handle with .png filename', async () => {
|
|
43
|
+
const item = buildPngDownloadItem({
|
|
44
|
+
filename: 'sales',
|
|
45
|
+
getCaptureEl: () => document.createElement('div'),
|
|
46
|
+
})
|
|
47
|
+
const handle = await item.resolve()
|
|
48
|
+
expect(handle.url).toBe('blob:png')
|
|
49
|
+
expect(handle.filename).toBe('sales.png')
|
|
50
|
+
handle.revoke?.()
|
|
51
|
+
expect(revokeSpy).toHaveBeenCalledWith('blob:png')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('resolve() rejects when getCaptureEl returns null', async () => {
|
|
55
|
+
const item = buildPngDownloadItem({
|
|
56
|
+
filename: 'demo',
|
|
57
|
+
getCaptureEl: () => null,
|
|
58
|
+
})
|
|
59
|
+
await expect(item.resolve()).rejects.toThrow(/No PNG capture element/)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('forwards pixelRatio and backgroundColor overrides', async () => {
|
|
63
|
+
const item = buildPngDownloadItem({
|
|
64
|
+
filename: 'sales',
|
|
65
|
+
getCaptureEl: () => document.createElement('div'),
|
|
66
|
+
pixelRatio: 4,
|
|
67
|
+
backgroundColor: '#fff',
|
|
68
|
+
})
|
|
69
|
+
const handle = await item.resolve()
|
|
70
|
+
expect(handle.filename).toBe('sales.png')
|
|
71
|
+
})
|
|
72
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { downloadDOMToPNG } from './exports'
|
|
2
|
+
import { PNGIcon } from './icons'
|
|
3
|
+
import type { DownloadItem } from './types'
|
|
4
|
+
|
|
5
|
+
export interface BuildPngDownloadItemArgs {
|
|
6
|
+
/** Base filename (without extension). The item appends `.png`. */
|
|
7
|
+
filename: string
|
|
8
|
+
/**
|
|
9
|
+
* Reads the capture element to rasterise. Called at click time so the
|
|
10
|
+
* download config doesn't capture a stale reference. Wire it to
|
|
11
|
+
* `() => getCaptureEl(id)` from `widgets-v2/stores`.
|
|
12
|
+
*/
|
|
13
|
+
getCaptureEl: () => HTMLElement | null
|
|
14
|
+
/** html2canvas `scale`. Default 2. */
|
|
15
|
+
pixelRatio?: number
|
|
16
|
+
/** html2canvas `backgroundColor`. Default transparent (`null`). */
|
|
17
|
+
backgroundColor?: string | null
|
|
18
|
+
/** Override the menu label. Default `'PNG'`. */
|
|
19
|
+
label?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Builds the standard PNG `DownloadItem` used by every per-widget download
|
|
24
|
+
* config. Centralised so the menu label, icon, error message, and filename
|
|
25
|
+
* suffix stay consistent across widgets without each `create*DownloadConfig`
|
|
26
|
+
* re-deriving the same shape.
|
|
27
|
+
*/
|
|
28
|
+
export function buildPngDownloadItem(
|
|
29
|
+
args: BuildPngDownloadItemArgs,
|
|
30
|
+
): DownloadItem {
|
|
31
|
+
return {
|
|
32
|
+
id: 'png',
|
|
33
|
+
label: args.label ?? 'PNG',
|
|
34
|
+
icon: <PNGIcon fontSize='small' />,
|
|
35
|
+
resolve: async () => {
|
|
36
|
+
const el = args.getCaptureEl()
|
|
37
|
+
if (!el) {
|
|
38
|
+
throw new Error('[widgets-v2] No PNG capture element available')
|
|
39
|
+
}
|
|
40
|
+
const handle = await downloadDOMToPNG({
|
|
41
|
+
element: el,
|
|
42
|
+
pixelRatio: args.pixelRatio,
|
|
43
|
+
backgroundColor: args.backgroundColor,
|
|
44
|
+
})
|
|
45
|
+
return {
|
|
46
|
+
url: handle.url,
|
|
47
|
+
filename: `${args.filename}.png`,
|
|
48
|
+
revoke: handle.revoke,
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ReactElement } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface DownloadItem {
|
|
4
|
+
/** Unique key (also used as menu label key). */
|
|
5
|
+
id: string
|
|
6
|
+
/** Display label in the menu. */
|
|
7
|
+
label: string
|
|
8
|
+
/**
|
|
9
|
+
* Optional leading icon shown next to the label. Pass a rendered element
|
|
10
|
+
* (e.g. `<DownloadIcon fontSize='small' />`) so consumers can fully
|
|
11
|
+
* control sizing / colour. Rendered inside an MUI `<ListItemIcon>`.
|
|
12
|
+
*/
|
|
13
|
+
icon?: ReactElement
|
|
14
|
+
/**
|
|
15
|
+
* When `true`, the menu item is rendered but non-clickable (greyed out).
|
|
16
|
+
* Useful for items whose underlying export is unavailable in the current
|
|
17
|
+
* widget state (no rows, missing permission, etc.).
|
|
18
|
+
*/
|
|
19
|
+
disabled?: boolean
|
|
20
|
+
/**
|
|
21
|
+
* Override for the resolved filename. When set, `resolve()`'s `filename`
|
|
22
|
+
* is ignored and this value is used instead — handy when the consumer
|
|
23
|
+
* wants to template the filename with widget metadata without having to
|
|
24
|
+
* close over it inside `resolve()`.
|
|
25
|
+
*/
|
|
26
|
+
filename?: string
|
|
27
|
+
/**
|
|
28
|
+
* Resolves to a Blob URL the trigger will click. Optional `revoke` is
|
|
29
|
+
* called after the click is dispatched.
|
|
30
|
+
*/
|
|
31
|
+
resolve: () => Promise<{ url: string; filename: string; revoke?: () => void }>
|
|
32
|
+
}
|