From 944a40bdb593b1c32ec404b3f19b6d5c776bb3f4 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Thu, 31 Aug 2023 15:48:24 +0200 Subject: [PATCH 01/40] chore: Clean up --- app/components/chart-panel.tsx | 8 ++--- app/components/chart-selection-tabs.tsx | 13 ++++---- app/config-types.ts | 40 ++++++++++++++----------- 3 files changed, 31 insertions(+), 30 deletions(-) diff --git a/app/components/chart-panel.tsx b/app/components/chart-panel.tsx index ac8d087eb..d214f946a 100644 --- a/app/components/chart-panel.tsx +++ b/app/components/chart-panel.tsx @@ -22,10 +22,7 @@ export const ChartPanelConfigurator = (props: ChartPanelProps) => { return ( <> - + ); @@ -38,8 +35,7 @@ export const ChartPanelPublished = ( return ( <> - {/* TODO: Re-enable in the future, when chart composition is implemented */} - {/* */} + ); diff --git a/app/components/chart-selection-tabs.tsx b/app/components/chart-selection-tabs.tsx index 10be1b374..b6b2a351f 100644 --- a/app/components/chart-selection-tabs.tsx +++ b/app/components/chart-selection-tabs.tsx @@ -123,7 +123,7 @@ const TabsEditable = ({ chartType }: { chartType: ChartType }) => { <> ) => { setPopoverAnchorEl(e.currentTarget); setTabsState({ isPopoverOpen: true }); @@ -149,7 +149,7 @@ const TabsEditable = ({ chartType }: { chartType: ChartType }) => { }; const TabsFixed = ({ chartType }: { chartType: ChartType }) => { - return ; + return ; }; const PublishChartButton = () => { @@ -201,11 +201,11 @@ const PublishChartButton = () => { const TabsInner = ({ chartType, - editable, + editable = false, onActionButtonClick, }: { chartType: ChartType; - editable: boolean; + editable?: boolean; onActionButtonClick?: (e: React.MouseEvent) => void; }) => { return ( @@ -215,7 +215,6 @@ const TabsInner = ({ TabIndicatorProps={{ style: { display: "none" } }} sx={{ position: "relative", top: 1, flexGrow: 1 }} > - {/* TODO: Generate dynamically when chart composition is implemented. Add useStyles */} { const classes = useStyles({ editable }); diff --git a/app/config-types.ts b/app/config-types.ts index a0bde0740..77d6394cc 100644 --- a/app/config-types.ts +++ b/app/config-types.ts @@ -894,70 +894,76 @@ const DataSource = t.type({ type: t.union([t.literal("sql"), t.literal("sparql")]), url: t.string, }); - export type DataSource = t.TypeOf; const Config = t.type( { dataSet: t.string, dataSource: DataSource, - activeField: t.union([t.string, t.undefined]), meta: Meta, chartConfig: ChartConfig, + activeField: t.union([t.string, t.undefined]), }, "Config" ); - export type Config = t.TypeOf; -export const isValidConfig = (config: unknown): config is Config => - Config.is(config); +export const isValidConfig = (config: unknown): config is Config => { + return Config.is(config); +}; -export const decodeConfig = (config: unknown) => Config.decode(config); +export const decodeConfig = (config: unknown) => { + return Config.decode(config); +}; const ConfiguratorStateInitial = t.type({ state: t.literal("INITIAL"), - activeField: t.undefined, dataSet: t.undefined, dataSource: DataSource, + activeField: t.undefined, }); +export type ConfiguratorStateInitial = t.TypeOf< + typeof ConfiguratorStateInitial +>; + const ConfiguratorStateSelectingDataSet = t.type({ state: t.literal("SELECTING_DATASET"), - activeField: t.undefined, - meta: Meta, dataSet: t.union([t.string, t.undefined]), dataSource: DataSource, + meta: Meta, chartConfig: t.undefined, + activeField: t.undefined, }); +export type ConfiguratorStateSelectingDataSet = t.TypeOf< + typeof ConfiguratorStateSelectingDataSet +>; + const ConfiguratorStateConfiguringChart = t.intersection([ t.type({ state: t.literal("CONFIGURING_CHART"), }), Config, ]); +export type ConfiguratorStateConfiguringChart = t.TypeOf< + typeof ConfiguratorStateConfiguringChart +>; + const ConfiguratorStatePublishing = t.intersection([ t.type({ state: t.literal("PUBLISHING"), }), Config, ]); - -export type ConfiguratorStateSelectingDataSet = t.TypeOf< - typeof ConfiguratorStateSelectingDataSet ->; -export type ConfiguratorStateConfiguringChart = t.TypeOf< - typeof ConfiguratorStateConfiguringChart ->; export type ConfiguratorStatePublishing = t.TypeOf< typeof ConfiguratorStatePublishing >; + const ConfiguratorState = t.union([ ConfiguratorStateInitial, ConfiguratorStateSelectingDataSet, ConfiguratorStateConfiguringChart, ConfiguratorStatePublishing, ]); - export type ConfiguratorState = t.TypeOf; export const decodeConfiguratorState = ( From eddc3dca1f79a441e23fefa955f52514d8c9bc0a Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Thu, 31 Aug 2023 16:29:40 +0200 Subject: [PATCH 02/40] refactor: Add GenericChartConfig type --- app/config-types.ts | 159 ++++++++++++++++++++++++-------------------- 1 file changed, 86 insertions(+), 73 deletions(-) diff --git a/app/config-types.ts b/app/config-types.ts index 77d6394cc..d2fc755e1 100644 --- a/app/config-types.ts +++ b/app/config-types.ts @@ -222,6 +222,12 @@ const SortingField = t.partial({ }); export type SortingField = t.TypeOf; +const GenericChartConfig = t.type({ + version: t.string, + filters: Filters, +}); +export type GenericChartConfig = t.TypeOf; + const ChartSubType = t.union([t.literal("stacked"), t.literal("grouped")]); export type ChartSubType = t.TypeOf; @@ -242,16 +248,17 @@ const ColumnFields = t.intersection([ animation: AnimationField, }), ]); -const ColumnConfig = t.type( - { - version: t.string, - chartType: t.literal("column"), - filters: Filters, - interactiveFiltersConfig: InteractiveFiltersConfig, - fields: ColumnFields, - }, - "ColumnConfig" -); +const ColumnConfig = t.intersection([ + GenericChartConfig, + t.type( + { + chartType: t.literal("column"), + interactiveFiltersConfig: InteractiveFiltersConfig, + fields: ColumnFields, + }, + "ColumnConfig" + ), +]); export type ColumnFields = t.TypeOf; export type ColumnConfig = t.TypeOf; @@ -267,16 +274,17 @@ const LineFields = t.intersection([ segment: LineSegmentField, }), ]); -const LineConfig = t.type( - { - version: t.string, - chartType: t.literal("line"), - filters: Filters, - interactiveFiltersConfig: InteractiveFiltersConfig, - fields: LineFields, - }, - "LineConfig" -); +const LineConfig = t.intersection([ + GenericChartConfig, + t.type( + { + chartType: t.literal("line"), + interactiveFiltersConfig: InteractiveFiltersConfig, + fields: LineFields, + }, + "LineConfig" + ), +]); export type LineFields = t.TypeOf; export type LineConfig = t.TypeOf; @@ -303,16 +311,17 @@ const AreaFields = t.intersection([ segment: AreaSegmentField, }), ]); -const AreaConfig = t.type( - { - version: t.string, - chartType: t.literal("area"), - filters: Filters, - interactiveFiltersConfig: InteractiveFiltersConfig, - fields: AreaFields, - }, - "AreaConfig" -); +const AreaConfig = t.intersection([ + GenericChartConfig, + t.type( + { + chartType: t.literal("area"), + interactiveFiltersConfig: InteractiveFiltersConfig, + fields: AreaFields, + }, + "AreaConfig" + ), +]); export type AreaFields = t.TypeOf; export type AreaConfig = t.TypeOf; @@ -329,16 +338,17 @@ const ScatterPlotFields = t.intersection([ animation: AnimationField, }), ]); -const ScatterPlotConfig = t.type( - { - version: t.string, - chartType: t.literal("scatterplot"), - filters: Filters, - interactiveFiltersConfig: InteractiveFiltersConfig, - fields: ScatterPlotFields, - }, - "ScatterPlotConfig" -); +const ScatterPlotConfig = t.intersection([ + GenericChartConfig, + t.type( + { + chartType: t.literal("scatterplot"), + interactiveFiltersConfig: InteractiveFiltersConfig, + fields: ScatterPlotFields, + }, + "ScatterPlotConfig" + ), +]); export type ScatterPlotFields = t.TypeOf; export type ScatterPlotConfig = t.TypeOf; @@ -353,16 +363,17 @@ const PieFields = t.intersection([ }), t.partial({ animation: AnimationField }), ]); -const PieConfig = t.type( - { - version: t.string, - chartType: t.literal("pie"), - interactiveFiltersConfig: InteractiveFiltersConfig, - filters: Filters, - fields: PieFields, - }, - "PieConfig" -); +const PieConfig = t.intersection([ + GenericChartConfig, + t.type( + { + chartType: t.literal("pie"), + interactiveFiltersConfig: InteractiveFiltersConfig, + fields: PieFields, + }, + "PieConfig" + ), +]); export type PieFields = t.TypeOf; export type PieConfig = t.TypeOf; @@ -470,18 +481,19 @@ const TableSortingOption = t.type({ }); export type TableSortingOption = t.TypeOf; -const TableConfig = t.type( - { - version: t.string, - chartType: t.literal("table"), - fields: TableFields, - filters: Filters, - settings: TableSettings, - sorting: t.array(TableSortingOption), - interactiveFiltersConfig: t.undefined, - }, - "TableConfig" -); +const TableConfig = t.intersection([ + GenericChartConfig, + t.type( + { + chartType: t.literal("table"), + fields: TableFields, + settings: TableSettings, + sorting: t.array(TableSortingOption), + interactiveFiltersConfig: t.undefined, + }, + "TableConfig" + ), +]); export type TableFields = t.TypeOf; export type TableConfig = t.TypeOf; @@ -573,17 +585,18 @@ const MapFields = t.partial({ animation: AnimationField, }); -const MapConfig = t.type( - { - version: t.string, - chartType: t.literal("map"), - interactiveFiltersConfig: InteractiveFiltersConfig, - filters: Filters, - fields: MapFields, - baseLayer: BaseLayer, - }, - "MapConfig" -); +const MapConfig = t.intersection([ + GenericChartConfig, + t.type( + { + chartType: t.literal("map"), + interactiveFiltersConfig: InteractiveFiltersConfig, + fields: MapFields, + baseLayer: BaseLayer, + }, + "MapConfig" + ), +]); export type MapFields = t.TypeOf; export type MapConfig = t.TypeOf; From f391595b66fa74aa702806a62e98a8f4488a0b4e Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Tue, 5 Sep 2023 12:14:07 +0200 Subject: [PATCH 03/40] feat: Initial implementation of multiple charts at once --- app/charts/chart-config-ui-options.ts | 119 +++--- app/charts/column/columns-stacked-state.tsx | 1 + app/charts/index.ts | 54 ++- app/charts/line/lines-state.tsx | 1 + app/charts/shared/legend-color.tsx | 8 +- app/components/chart-panel.tsx | 4 +- app/components/chart-preview.tsx | 16 +- app/components/chart-published.tsx | 2 +- app/components/chart-selection-tabs.tsx | 96 ++++- app/config-types.ts | 28 +- .../components/chart-annotations-selector.tsx | 11 +- .../components/chart-configurator.tsx | 35 +- .../chart-controls/color-palette.tsx | 26 +- .../components/chart-controls/color-ramp.tsx | 12 +- .../components/chart-options-selector.tsx | 104 ++--- app/configurator/components/configurator.tsx | 10 +- app/configurator/components/field.tsx | 13 +- app/configurator/components/filters.tsx | 29 +- app/configurator/config-form.tsx | 28 +- app/configurator/configurator-state.spec.tsx | 178 +++++---- app/configurator/configurator-state.tsx | 355 ++++++++++-------- .../interactive-filters-config-state.tsx | 71 ++-- .../interactive-filters-configurator.tsx | 5 +- .../table/table-chart-configurator.tsx | 30 +- .../table/table-chart-options.tsx | 29 +- .../table/table-chart-sorting-options.tsx | 10 +- app/db/config.ts | 25 +- app/docs/annotations.docs.tsx | 55 ++- app/docs/columns.docs.tsx | 19 +- app/docs/fixtures.ts | 104 +++-- app/docs/lines.docs.tsx | 21 +- app/docs/scatterplot.docs.tsx | 19 +- app/homepage/examples.tsx | 260 +++++++------ app/pages/__test/[env]/[slug].tsx | 4 +- app/pages/_charts.tsx | 10 +- app/pages/embed/[chartId].tsx | 14 +- app/pages/v/[chartId].tsx | 11 +- app/utils/chart-config/api.ts | 6 +- app/utils/chart-config/versioning.spec.ts | 32 +- app/utils/chart-config/versioning.ts | 194 +++++++--- 40 files changed, 1267 insertions(+), 782 deletions(-) diff --git a/app/charts/chart-config-ui-options.ts b/app/charts/chart-config-ui-options.ts index 9f56bc184..5c279d55e 100644 --- a/app/charts/chart-config-ui-options.ts +++ b/app/charts/chart-config-ui-options.ts @@ -20,7 +20,6 @@ import { ColumnConfig, ColumnSegmentField, ComponentType, - ConfiguratorStateConfiguringChart, LineConfig, MapConfig, PaletteType, @@ -61,9 +60,7 @@ export type EncodingFieldType = export type OnEncodingOptionChange = ( value: V, options: { - draft: Omit & { - chartConfig: T; - }; + chartConfig: T; dimensions: DimensionMetadataFragment[]; measures: DimensionMetadataFragment[]; } @@ -114,17 +111,17 @@ export type EncodingOption = export const makeOnColorComponentScaleTypeChange = ( type: "areaLayer" | "symbolLayer" ): OnEncodingOptionChange => { - const basePath = `chartConfig.fields.${type}`; + const basePath = `fields.${type}`; const interpolationTypePath = `${basePath}.color.interpolationType`; const nbClassPath = `${basePath}.color.nbClass`; - return (value, { draft }) => { + return (value, { chartConfig }) => { if (value === "continuous") { - setWith(draft, interpolationTypePath, "linear", Object); - unset(draft, nbClassPath); + setWith(chartConfig, interpolationTypePath, "linear", Object); + unset(chartConfig, nbClassPath); } else if (value === "discrete") { - setWith(draft, interpolationTypePath, "jenks", Object); - setWith(draft, nbClassPath, 3, Object); + setWith(chartConfig, interpolationTypePath, "jenks", Object); + setWith(chartConfig, nbClassPath, 3, Object); } }; }; @@ -132,20 +129,20 @@ export const makeOnColorComponentScaleTypeChange = ( export const makeOnColorComponentIriChange = ( type: "areaLayer" | "symbolLayer" ): OnEncodingOptionChange => { - const basePath = `chartConfig.fields.${type}`; + const basePath = `fields.${type}`; - return (iri, { draft, dimensions, measures }) => { + return (iri, { chartConfig, dimensions, measures }) => { const components = [...dimensions, ...measures]; let newField: ColorField = DEFAULT_FIXED_COLOR_FIELD; const component = components.find((d) => d.iri === iri); const currentColorComponentIri = get( - draft, + chartConfig, `${basePath}.color.componentIri` ); if (component) { const colorPalette: PaletteType | undefined = get( - draft, + chartConfig, `${basePath}.color.palette` ); @@ -178,8 +175,8 @@ export const makeOnColorComponentIriChange = ( } // Remove old filter. - unset(draft, `chartConfig.filters["${currentColorComponentIri}"]`); - setWith(draft, `${basePath}.color`, newField, Object); + unset(chartConfig, `filters["${currentColorComponentIri}"]`); + setWith(chartConfig, `${basePath}.color`, newField, Object); }; }; @@ -212,9 +209,7 @@ export type EncodingSortingOption = { export type OnEncodingChange = ( iri: string, options: { - draft: Omit & { - chartConfig: T; - }; + chartConfig: T; dimensions: DimensionMetadataFragment[]; measures: DimensionMetadataFragment[]; initializing: boolean; @@ -337,9 +332,9 @@ export const ANIMATION_FIELD_SPEC: EncodingSpec< componentTypes: ["TemporalDimension", "TemporalOrdinalDimension"], filters: true, disableInteractiveFilters: true, - onChange: (iri, { draft, initializing }) => { - if (initializing || !draft.chartConfig.fields.animation) { - draft.chartConfig.fields.animation = { + onChange: (iri, { chartConfig, initializing }) => { + if (initializing || !chartConfig.fields.animation) { + chartConfig.fields.animation = { componentIri: iri, showPlayButton: true, duration: 30, @@ -347,7 +342,7 @@ export const ANIMATION_FIELD_SPEC: EncodingSpec< dynamicScales: false, }; } else { - draft.chartConfig.fields.animation.componentIri = iri; + chartConfig.fields.animation.componentIri = iri; } }, getDisabledState: ( @@ -417,14 +412,16 @@ export const defaultSegmentOnChange: OnEncodingChange< | ScatterPlotConfig | PieConfig | TableConfig -> = (iri, { draft, dimensions, measures, initializing, selectedValues }) => { +> = ( + iri, + { chartConfig, dimensions, measures, initializing, selectedValues } +) => { const components = [...dimensions, ...measures]; const component = components.find((d) => d.iri === iri); const palette = getDefaultCategoricalPaletteName( component, - draft.chartConfig.fields.segment && - "palette" in draft.chartConfig.fields.segment - ? draft.chartConfig.fields.segment.palette + chartConfig.fields.segment && "palette" in chartConfig.fields.segment + ? chartConfig.fields.segment.palette : undefined ); const colorMapping = mapValueIrisToColor({ @@ -434,29 +431,29 @@ export const defaultSegmentOnChange: OnEncodingChange< const multiFilter = makeMultiFilter(selectedValues.map((d) => d.value)); if (initializing) { - draft.chartConfig.fields.segment = { + chartConfig.fields.segment = { componentIri: iri, palette, sorting: DEFAULT_SORTING, colorMapping, }; } else if ( - draft.chartConfig.fields.segment && - "palette" in draft.chartConfig.fields.segment + chartConfig.fields.segment && + "palette" in chartConfig.fields.segment ) { - draft.chartConfig.fields.segment.componentIri = iri; - draft.chartConfig.fields.segment.colorMapping = colorMapping; + chartConfig.fields.segment.componentIri = iri; + chartConfig.fields.segment.colorMapping = colorMapping; } - draft.chartConfig.filters[iri] = multiFilter; + chartConfig.filters[iri] = multiFilter; }; export const makeOnMapFieldChange = ( field: "areaLayer" | "symbolLayer" ): OnEncodingChange => { - return (iri, { draft, dimensions, measures }) => { + return (iri, { chartConfig, dimensions, measures }) => { initializeMapLayerField({ - chartConfig: draft.chartConfig, + chartConfig, field, componentIri: iri, dimensions, @@ -480,11 +477,11 @@ const chartConfigOptionsUISpec: ChartSpecs = { optional: false, componentTypes: ["NumericalMeasure"], filters: false, - onChange: (iri, { draft, measures }) => { + onChange: (iri, { chartConfig, measures }) => { const yMeasure = measures.find((d) => d.iri === iri); if (disableStacked(yMeasure)) { - delete draft.chartConfig.fields.segment; + delete chartConfig.fields.segment; } }, }, @@ -556,22 +553,17 @@ const chartConfigOptionsUISpec: ChartSpecs = { optional: false, componentTypes: ["NumericalMeasure"], filters: false, - onChange: (iri, { draft, measures }) => { - if (draft.chartConfig.fields.segment?.type === "stacked") { + onChange: (iri, { chartConfig, measures }) => { + if (chartConfig.fields.segment?.type === "stacked") { const yMeasure = measures.find((d) => d.iri === iri); if (disableStacked(yMeasure)) { - setWith( - draft, - "chartConfig.fields.segment.type", - "grouped", - Object - ); - - if (draft.chartConfig.interactiveFiltersConfig?.calculation) { + setWith(chartConfig, "fields.segment.type", "grouped", Object); + + if (chartConfig.interactiveFiltersConfig?.calculation) { setWith( - draft, - "chartConfig.interactiveFiltersConfig.calculation", + chartConfig, + "interactiveFiltersConfig.calculation", { active: false, type: "identity" }, Object ); @@ -600,13 +592,13 @@ const chartConfigOptionsUISpec: ChartSpecs = { { sortingType: "byMeasure", sortingOrder: ["asc", "desc"] }, { sortingType: "byDimensionLabel", sortingOrder: ["asc", "desc"] }, ], - onChange: (iri, { draft, dimensions }) => { + onChange: (iri, { chartConfig, dimensions }) => { const component = dimensions.find((d) => d.iri === iri); if (!isTemporalDimension(component)) { setWith( - draft, - `chartConfig.interactiveFiltersConfig.timeRange.active`, + chartConfig, + "interactiveFiltersConfig.timeRange.active", false, Object ); @@ -623,7 +615,7 @@ const chartConfigOptionsUISpec: ChartSpecs = { filters: true, sorting: COLUMN_SEGMENT_SORTING, onChange: (iri, options) => { - const { draft, dimensions, measures, initializing } = options; + const { chartConfig, dimensions, measures, initializing } = options; defaultSegmentOnChange(iri, options); if (!initializing) { @@ -632,15 +624,15 @@ const chartConfigOptionsUISpec: ChartSpecs = { const components = [...dimensions, ...measures]; const segment: ColumnSegmentField = get( - draft, - "chartConfig.fields.segment" + chartConfig, + "fields.segment" ); const yComponent = components.find( - (d) => d.iri === draft.chartConfig.fields.y.componentIri + (d) => d.iri === chartConfig.fields.y.componentIri ); setWith( - draft, - "chartConfig.fields.segment", + chartConfig, + "fields.segment", { ...segment, type: disableStacked(yComponent) ? "grouped" : "stacked", @@ -689,13 +681,10 @@ const chartConfigOptionsUISpec: ChartSpecs = { }, ]; }, - onChange: (d, { draft }) => { - if ( - draft.chartConfig.interactiveFiltersConfig && - d === "grouped" - ) { - const path = "chartConfig.interactiveFiltersConfig.calculation"; - setWith(draft, path, { active: false, type: "identity" }); + onChange: (d, { chartConfig }) => { + if (chartConfig.interactiveFiltersConfig && d === "grouped") { + const path = "interactiveFiltersConfig.calculation"; + setWith(chartConfig, path, { active: false, type: "identity" }); } }, }, diff --git a/app/charts/column/columns-stacked-state.tsx b/app/charts/column/columns-stacked-state.tsx index ec167ee8e..7ccdc3dbe 100644 --- a/app/charts/column/columns-stacked-state.tsx +++ b/app/charts/column/columns-stacked-state.tsx @@ -85,6 +85,7 @@ const useColumnsStackedState = ( data: ColumnsStackedStateData ): StackedColumnsState => { const { aspectRatio, chartConfig } = chartProps; + console.log(chartConfig); const { xDimension, getX, diff --git a/app/charts/index.ts b/app/charts/index.ts index 3ebf1a4dc..370e0f616 100644 --- a/app/charts/index.ts +++ b/app/charts/index.ts @@ -28,6 +28,7 @@ import { MapAreaLayer, MapConfig, MapSymbolLayer, + Meta, PieSegmentField, ScatterPlotSegmentField, SortingOrder, @@ -40,6 +41,7 @@ import { HierarchyValue } from "@/graphql/resolver-types"; import { getDefaultCategoricalPaletteName } from "@/palettes"; import { bfs } from "@/utils/bfs"; import { CHART_CONFIG_VERSION } from "@/utils/chart-config/versioning"; +import { createChartId } from "@/utils/create-chart-id"; import { isMultiHierarchyNode } from "@/utils/hierarchy"; import { mapValueIrisToColor } from "../configurator/components/ui-helpers"; @@ -269,6 +271,21 @@ export const getInitialSymbolLayer = ({ }; }; +const META: Meta = { + title: { + en: "", + de: "", + fr: "", + it: "", + }, + description: { + en: "", + de: "", + fr: "", + it: "", + }, +}; + export const getInitialConfig = ({ chartType, dimensions, @@ -278,6 +295,17 @@ export const getInitialConfig = ({ dimensions: DataCubeMetadataWithHierarchies["dimensions"]; measures: DataCubeMetadataWithHierarchies["measures"]; }): ChartConfig => { + const genericConfigProps: { + key: string; + version: string; + meta: Meta; + activeField: string | undefined; + } = { + key: createChartId(), + version: CHART_CONFIG_VERSION, + meta: META, + activeField: undefined, + }; const numericalMeasures = measures.filter(isNumericalMeasure); switch (chartType) { @@ -285,7 +313,7 @@ export const getInitialConfig = ({ const areaXComponentIri = getTemporalDimensions(dimensions)[0].iri; return { - version: CHART_CONFIG_VERSION, + ...genericConfigProps, chartType, filters: {}, interactiveFiltersConfig: getInitialInteractiveFiltersConfig({ @@ -305,7 +333,7 @@ export const getInitialConfig = ({ ).iri; return { - version: CHART_CONFIG_VERSION, + ...genericConfigProps, chartType, filters: {}, interactiveFiltersConfig: getInitialInteractiveFiltersConfig({ @@ -323,7 +351,7 @@ export const getInitialConfig = ({ const lineXComponentIri = getTemporalDimensions(dimensions)[0].iri; return { - version: CHART_CONFIG_VERSION, + ...genericConfigProps, chartType, filters: {}, interactiveFiltersConfig: getInitialInteractiveFiltersConfig({ @@ -342,7 +370,7 @@ export const getInitialConfig = ({ const showSymbolLayer = !showAreaLayer; return { - version: CHART_CONFIG_VERSION, + ...genericConfigProps, chartType, filters: makeInitialFiltersForArea(areaDimension), interactiveFiltersConfig: getInitialInteractiveFiltersConfig(), @@ -377,7 +405,7 @@ export const getInitialConfig = ({ const piePalette = getDefaultCategoricalPaletteName(pieSegmentComponent); return { - version: CHART_CONFIG_VERSION, + ...genericConfigProps, chartType, filters: {}, interactiveFiltersConfig: getInitialInteractiveFiltersConfig(), @@ -403,7 +431,7 @@ export const getInitialConfig = ({ ); return { - version: CHART_CONFIG_VERSION, + ...genericConfigProps, chartType: "scatterplot", filters: {}, interactiveFiltersConfig: getInitialInteractiveFiltersConfig(), @@ -433,7 +461,7 @@ export const getInitialConfig = ({ ); return { - version: CHART_CONFIG_VERSION, + ...genericConfigProps, chartType, filters: {}, interactiveFiltersConfig: undefined, @@ -487,11 +515,15 @@ export const getChartConfigAdjustedToChartType = ({ measures, }); const { interactiveFiltersConfig, ...rest } = chartConfig; - const newChartConfig = getAdjustedChartConfig({ + + return getAdjustedChartConfig({ path: "", // Make sure interactiveFiltersConfig is passed as the last item, so that // it can be adjusted based on other, already adjusted fields. - field: { ...rest, interactiveFiltersConfig }, + field: { + ...rest, + interactiveFiltersConfig, + }, adjusters: chartConfigsAdjusters[newChartType], pathOverrides: chartConfigsPathOverrides[newChartType][oldChartType], oldChartConfig: chartConfig, @@ -499,8 +531,6 @@ export const getChartConfigAdjustedToChartType = ({ dimensions, measures, }); - - return newChartConfig; }; const getAdjustedChartConfig = ({ @@ -819,7 +849,7 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { "sorting" in oldSegment && oldSegment.sorting && "sortingOrder" in oldSegment.sorting - ? oldSegment.sorting || DEFAULT_FIXED_COLOR_FIELD + ? oldSegment.sorting ?? DEFAULT_FIXED_COLOR_FIELD : DEFAULT_SORTING, }; } diff --git a/app/charts/line/lines-state.tsx b/app/charts/line/lines-state.tsx index 623cb3ed5..d1d0deeee 100644 --- a/app/charts/line/lines-state.tsx +++ b/app/charts/line/lines-state.tsx @@ -70,6 +70,7 @@ const useLinesState = ( data: ChartStateData ): LinesState => { const { chartConfig, aspectRatio } = chartProps; + console.log(chartConfig); const { xDimension, getX, diff --git a/app/charts/shared/legend-color.tsx b/app/charts/shared/legend-color.tsx index 6e0f5eb69..1861e4ac6 100644 --- a/app/charts/shared/legend-color.tsx +++ b/app/charts/shared/legend-color.tsx @@ -14,6 +14,7 @@ import { DataSource, GenericSegmentField, MapConfig, + getChartConfig, isSegmentInConfig, useReadOnlyConfiguratorState, } from "@/configurator"; @@ -148,16 +149,15 @@ const useLegendGroups = ({ } const locale = useLocale(); + const chartConfig = getChartConfig(configState); // FIXME: should color field also be included here? const segmentField = ( - isSegmentInConfig(configState.chartConfig) - ? configState.chartConfig.fields.segment - : null + isSegmentInConfig(chartConfig) ? chartConfig.fields.segment : null ) as GenericSegmentField | null | undefined; const segmentFilters = segmentField?.componentIri - ? configState.chartConfig.filters[segmentField.componentIri] + ? chartConfig.filters[segmentField.componentIri] : null; const segmentValues = segmentFilters?.type === "multi" ? segmentFilters.values : emptyObj; diff --git a/app/components/chart-panel.tsx b/app/components/chart-panel.tsx index d214f946a..672ae7315 100644 --- a/app/components/chart-panel.tsx +++ b/app/components/chart-panel.tsx @@ -7,6 +7,7 @@ import { ChartType, ConfiguratorStateConfiguringChart, ConfiguratorStatePublishing, + getChartConfig, } from "@/configurator"; import { useConfiguratorState } from "@/src"; @@ -19,10 +20,11 @@ export const ChartPanelConfigurator = (props: ChartPanelProps) => { const [state] = useConfiguratorState() as unknown as [ ConfiguratorStateConfiguringChart | ConfiguratorStatePublishing ]; + const chartConfig = getChartConfig(state); return ( <> - + ); diff --git a/app/components/chart-preview.tsx b/app/components/chart-preview.tsx index 1fb327ce8..43dfb4325 100644 --- a/app/components/chart-preview.tsx +++ b/app/components/chart-preview.tsx @@ -16,7 +16,11 @@ import DebugPanel from "@/components/debug-panel"; import Flex from "@/components/flex"; import { HintYellow } from "@/components/hint"; import { MetadataPanel } from "@/components/metadata-panel"; -import { DataSource, useConfiguratorState } from "@/configurator"; +import { + DataSource, + getChartConfig, + useConfiguratorState, +} from "@/configurator"; import { useComponentsQuery, useDataCubeMetadataQuery, @@ -60,6 +64,8 @@ const useStyles = makeStyles({ export const ChartPreviewInner = (props: ChartPreviewProps) => { const { dataSetIri, dataSource } = props; const [state, dispatch] = useConfiguratorState(); + const chartConfig = getChartConfig(state); + console.log(state.activeChartKey, chartConfig); const locale = useLocale(); const classes = useStyles(); const [{ data: metadata }] = useDataCubeMetadataQuery({ @@ -199,22 +205,22 @@ export const ChartPreviewInner = (props: ChartPreviewProps) => { }} dataSetIri={dataSetIri} dataSource={dataSource} - chartConfig={state.chartConfig} + chartConfig={chartConfig} /> ) : ( )} - {state.chartConfig && ( + {chartConfig && ( )} diff --git a/app/components/chart-published.tsx b/app/components/chart-published.tsx index fd034c261..19be75289 100644 --- a/app/components/chart-published.tsx +++ b/app/components/chart-published.tsx @@ -155,7 +155,7 @@ export const ChartPublishedInner = (props: ChartPublishInnerProps) => { state: "PUBLISHING", dataSet, dataSource, - chartConfig, + chartConfigs: [chartConfig], } as ConfiguratorStatePublishing; }, [dataSet, dataSource, chartConfig]); const handleToggleTableView = useEvent(() => setIsTablePreview((c) => !c)); diff --git a/app/components/chart-selection-tabs.tsx b/app/components/chart-selection-tabs.tsx index b6b2a351f..ed8e4212b 100644 --- a/app/components/chart-selection-tabs.tsx +++ b/app/components/chart-selection-tabs.tsx @@ -14,6 +14,8 @@ import { ChartType, ConfiguratorStateConfiguringChart, ConfiguratorStatePublishing, + getChartConfig, + isConfiguring, useConfiguratorState, } from "@/configurator"; import { ChartTypeSelector } from "@/configurator/components/chart-type-selector"; @@ -24,6 +26,7 @@ import { } from "@/graphql/query-hooks"; import { Icon, IconName } from "@/icons"; import { useLocale } from "@/src"; +import { createChartId } from "@/utils/create-chart-id"; import useEvent from "@/utils/use-event"; import Flex from "./flex"; @@ -99,9 +102,8 @@ const useStyles = makeStyles((theme) => ({ })); const TabsEditable = ({ chartType }: { chartType: ChartType }) => { - const [configuratorState] = useConfiguratorState() as unknown as [ - ConfiguratorStateConfiguringChart | ConfiguratorStatePublishing - ]; + const [state, dispatch] = useConfiguratorState(isConfiguring); + const chartConfig = getChartConfig(state); const [tabsState, setTabsState] = useTabsState(); const [popoverAnchorEl, setPopoverAnchorEl] = useState( null @@ -117,17 +119,40 @@ const TabsEditable = ({ chartType }: { chartType: ChartType }) => { useEffect(() => { handleClose(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [configuratorState.chartConfig.chartType]); + }, [chartConfig.chartType]); return ( <> { + return { + key: d.key, + chartType: d.chartType, + editable: true, + }; + })} onActionButtonClick={(e: React.MouseEvent) => { + e.stopPropagation(); setPopoverAnchorEl(e.currentTarget); setTabsState({ isPopoverOpen: true }); }} + onSwitchButtonClick={(key: string) => { + dispatch({ + type: "SWITCH_ACTIVE_CHART", + value: key, + }); + }} + onAddButtonClick={() => { + dispatch({ + type: "CHART_CONFIG_ADD", + value: { + chartConfig: { + ...chartConfig, + key: createChartId(), + }, + }, + }); + }} /> { > @@ -149,7 +174,7 @@ const TabsEditable = ({ chartType }: { chartType: ChartType }) => { }; const TabsFixed = ({ chartType }: { chartType: ChartType }) => { - return ; + return ; }; const PublishChartButton = () => { @@ -200,13 +225,15 @@ const PublishChartButton = () => { }; const TabsInner = ({ - chartType, - editable = false, + data, onActionButtonClick, + onSwitchButtonClick, + onAddButtonClick, }: { - chartType: ChartType; - editable?: boolean; + data: { key: string; chartType: ChartType; editable?: boolean }[]; onActionButtonClick?: (e: React.MouseEvent) => void; + onSwitchButtonClick?: (key: string) => void; + onAddButtonClick?: () => void; }) => { return ( @@ -215,6 +242,29 @@ const TabsInner = ({ TabIndicatorProps={{ style: { display: "none" } }} sx={{ position: "relative", top: 1, flexGrow: 1 }} > + {data.map((d) => ( + { + e.stopPropagation(); + onSwitchButtonClick?.(d.key); + }} + /> + } + /> + ))} - } + onClick={onAddButtonClick} + label={} /> @@ -237,19 +285,27 @@ const TabsInner = ({ const TabContent = ({ iconName, editable, + onEditClick, + onSwitchClick, }: { iconName: IconName; editable: boolean; + onEditClick?: (e: React.MouseEvent) => void; + onSwitchClick?: (e: React.MouseEvent) => void; }) => { const classes = useStyles({ editable }); return ( - + {editable && ( - - - + )} ); diff --git a/app/config-types.ts b/app/config-types.ts index d2fc755e1..1edf06a2e 100644 --- a/app/config-types.ts +++ b/app/config-types.ts @@ -223,8 +223,11 @@ const SortingField = t.partial({ export type SortingField = t.TypeOf; const GenericChartConfig = t.type({ + key: t.string, version: t.string, + meta: Meta, filters: Filters, + activeField: t.union([t.string, t.undefined]), }); export type GenericChartConfig = t.TypeOf; @@ -911,11 +914,12 @@ export type DataSource = t.TypeOf; const Config = t.type( { + version: t.string, dataSet: t.string, dataSource: DataSource, meta: Meta, - chartConfig: ChartConfig, - activeField: t.union([t.string, t.undefined]), + chartConfigs: t.array(ChartConfig), + activeChartKey: t.union([t.string, t.undefined]), }, "Config" ); @@ -930,22 +934,23 @@ export const decodeConfig = (config: unknown) => { }; const ConfiguratorStateInitial = t.type({ + version: t.string, state: t.literal("INITIAL"), dataSet: t.undefined, dataSource: DataSource, - activeField: t.undefined, }); export type ConfiguratorStateInitial = t.TypeOf< typeof ConfiguratorStateInitial >; const ConfiguratorStateSelectingDataSet = t.type({ + version: t.string, state: t.literal("SELECTING_DATASET"), dataSet: t.union([t.string, t.undefined]), dataSource: DataSource, meta: Meta, - chartConfig: t.undefined, - activeField: t.undefined, + chartConfigs: t.undefined, + activeChartKey: t.undefined, }); export type ConfiguratorStateSelectingDataSet = t.TypeOf< typeof ConfiguratorStateSelectingDataSet @@ -993,3 +998,16 @@ export const decodeConfiguratorState = ( ) ); }; + +export const getChartConfig = (state: ConfiguratorState, chartKey?: string) => { + if (state.state === "INITIAL" || state.state === "SELECTING_DATASET") { + throw new Error("No chart config available"); + } + + return ( + state.chartConfigs.find( + (d) => d.key === (chartKey ?? state.activeChartKey) + // FIXME: color legend currently is scoped to the chart config, it shouldn't + ) ?? (state.chartConfigs[0] as ChartConfig) + ); +}; diff --git a/app/configurator/components/chart-annotations-selector.tsx b/app/configurator/components/chart-annotations-selector.tsx index abb7422c0..f74394af5 100644 --- a/app/configurator/components/chart-annotations-selector.tsx +++ b/app/configurator/components/chart-annotations-selector.tsx @@ -11,14 +11,18 @@ import { getFieldLabel } from "@/configurator/components/field-i18n"; import { locales } from "@/locales/locales"; import { useLocale } from "@/locales/use-locale"; -import { ConfiguratorStateConfiguringChart } from "../../config-types"; +import { + ConfiguratorStateConfiguringChart, + getChartConfig, +} from "../../config-types"; const TitleAndDescriptionOptions = ({ state, }: { state: ConfiguratorStateConfiguringChart; }) => { - const { activeField, meta } = state; + const chartConfig = getChartConfig(state); + const { activeField, meta } = chartConfig; const locale = useLocale(); // Reorder locales so the input field for @@ -56,7 +60,8 @@ export const ChartAnnotationsSelector = ({ }: { state: ConfiguratorStateConfiguringChart; }) => { - const { activeField } = state; + const chartConfig = getChartConfig(state); + const { activeField } = chartConfig; const panelRef = useRef(null); useEffect(() => { diff --git a/app/configurator/components/chart-configurator.tsx b/app/configurator/components/chart-configurator.tsx index 976007c4e..77c6884c1 100644 --- a/app/configurator/components/chart-configurator.tsx +++ b/app/configurator/components/chart-configurator.tsx @@ -38,6 +38,7 @@ import { ConfiguratorStateConfiguringChart, ConfiguratorStatePublishing, DataSource, + getChartConfig, isMapConfig, } from "@/configurator"; import { TitleAndDescriptionConfigurator } from "@/configurator/components/chart-annotator"; @@ -166,6 +167,7 @@ const useEnsurePossibleFilters = ({ state: ConfiguratorStateConfiguringChart | ConfiguratorStatePublishing; }) => { const [, dispatch] = useConfiguratorState(); + const chartConfig = getChartConfig(state); const [fetching, setFetching] = useState(false); const [error, setError] = useState(); const lastFilters = useRef(); @@ -173,9 +175,8 @@ const useEnsurePossibleFilters = ({ useEffect(() => { const run = async () => { - const { mappedFilters, unmappedFilters } = getFiltersByMappingStatus( - state.chartConfig - ); + const { mappedFilters, unmappedFilters } = + getFiltersByMappingStatus(chartConfig); if ( lastFilters.current && orderedIsEqual(lastFilters.current, unmappedFilters) @@ -218,7 +219,7 @@ const useEnsurePossibleFilters = ({ mappedFilters ); - if (!isEqual(filters, state.chartConfig.filters) && !isEmpty(filters)) { + if (!isEqual(filters, chartConfig.filters) && !isEmpty(filters)) { dispatch({ type: "CHART_CONFIG_FILTERS_UPDATE", value: { @@ -232,9 +233,7 @@ const useEnsurePossibleFilters = ({ }, [ client, dispatch, - state, - state.chartConfig.fields, - state.chartConfig.filters, + chartConfig, state.dataSet, state.dataSource.type, state.dataSource.url, @@ -253,12 +252,13 @@ const useFilterReorder = ({ onAddDimensionFilter?: () => void; }) => { const [state, dispatch] = useConfiguratorState(isConfiguring); + const chartConfig = getChartConfig(state); const locale = useLocale(); - const { filters } = state.chartConfig; + const { filters } = chartConfig; const { unmappedFilters, mappedFiltersIris } = useMemo(() => { - return getFiltersByMappingStatus(state.chartConfig); - }, [state.chartConfig]); + return getFiltersByMappingStatus(chartConfig); + }, [chartConfig]); const variables = useMemo(() => { const hasUnmappedFilters = Object.keys(unmappedFilters).length > 0; @@ -328,7 +328,7 @@ const useFilterReorder = ({ } const dimension = dimensions.find((d) => d.iri === dimensionIri); - const chartConfig = moveFilterField(state.chartConfig, { + const newChartConfig = moveFilterField(chartConfig, { dimensionIri, delta, possibleValues: dimension ? dimension.values : [], @@ -337,7 +337,7 @@ const useFilterReorder = ({ dispatch({ type: "CHART_CONFIG_REPLACED", value: { - chartConfig, + chartConfig: newChartConfig, dataSetMetadata: data, }, }); @@ -514,14 +514,14 @@ const InteractiveDataFilterCheckbox = ({ const FiltersBadge = ({ sx }: { sx?: BadgeProps["sx"] }) => { const ctx = useControlSectionContext(); const [state] = useConfiguratorState(isConfiguring); + const chartConfig = getChartConfig(state); return ( d.type === "single" - ).length + Object.values(chartConfig.filters).filter((d) => d.type === "single") + .length } color="secondary" sx={{ display: "block", ...sx }} @@ -534,6 +534,7 @@ export const ChartConfigurator = ({ }: { state: ConfiguratorStateConfiguringChart; }) => { + const chartConfig = getChartConfig(state); const { isOpen: isFilterMenuOpen, open: openFilterMenu, @@ -592,7 +593,7 @@ export const ChartConfigurator = ({ > @@ -730,7 +731,7 @@ export const ChartConfigurator = ({ )} - {state.chartConfig.chartType !== "table" && ( + {chartConfig.chartType !== "table" && ( )} diff --git a/app/configurator/components/chart-controls/color-palette.tsx b/app/configurator/components/chart-controls/color-palette.tsx index f05a83943..c91c00cd4 100644 --- a/app/configurator/components/chart-controls/color-palette.tsx +++ b/app/configurator/components/chart-controls/color-palette.tsx @@ -2,11 +2,11 @@ import { Trans } from "@lingui/macro"; import { Box, Button, - Typography, - Select, MenuItem, + Select, SelectProps, Theme, + Typography, } from "@mui/material"; import { makeStyles } from "@mui/styles"; import get from "lodash/get"; @@ -17,14 +17,16 @@ import Flex from "@/components/flex"; import { Label } from "@/components/form"; import { ConfiguratorStateConfiguringChart, + getChartConfig, + isConfiguring, useConfiguratorState, } from "@/configurator"; import { mapValueIrisToColor } from "@/configurator/components/ui-helpers"; import { isNumericalMeasure } from "@/domain/data"; import { DimensionMetadataFragment } from "@/graphql/query-hooks"; import { - categoricalPalettes, DEFAULT_CATEGORICAL_PALETTE_NAME, + categoricalPalettes, divergingSteppedPalettes, getDefaultCategoricalPalette, getPalette, @@ -58,7 +60,8 @@ export const ColorPalette = ({ colorConfigPath, component, }: Props) => { - const [state, dispatch] = useConfiguratorState(); + const [state, dispatch] = useConfiguratorState(isConfiguring); + const chartConfig = getChartConfig(state); const classes = useStyles(); const hasColors = hasDimensionColors(component); const defaultPalette = @@ -75,8 +78,8 @@ export const ColorPalette = ({ : categoricalPalettes; const currentPaletteName = get( - state, - `chartConfig.fields["${state.activeField}"].${ + chartConfig, + `fields["${chartConfig.activeField}"].${ colorConfigPath ? `${colorConfigPath}.` : "" }palette` ); @@ -206,18 +209,17 @@ const ColorPaletteReset = ({ state: ConfiguratorStateConfiguringChart; }) => { const [, dispatch] = useConfiguratorState(); + const chartConfig = getChartConfig(state); const palette = get( - state, - `chartConfig.fields["${field}"].${ - colorConfigPath ? `${colorConfigPath}.` : "" - }palette`, + chartConfig, + `fields["${field}"].${colorConfigPath ? `${colorConfigPath}.` : ""}palette`, DEFAULT_CATEGORICAL_PALETTE_NAME ) as string; const colorMapping = get( - state, - `chartConfig.fields["${field}"].${ + chartConfig, + `fields["${field}"].${ colorConfigPath ? `${colorConfigPath}.` : "" }colorMapping` ) as Record | undefined; diff --git a/app/configurator/components/chart-controls/color-ramp.tsx b/app/configurator/components/chart-controls/color-ramp.tsx index bdfce71f2..c97a63539 100644 --- a/app/configurator/components/chart-controls/color-ramp.tsx +++ b/app/configurator/components/chart-controls/color-ramp.tsx @@ -14,6 +14,8 @@ import { Label } from "@/components/form"; import { DivergingPaletteType, SequentialPaletteType, + getChartConfig, + isConfiguring, useConfiguratorState, } from "@/configurator"; import { useLocale } from "@/locales/use-locale"; @@ -75,7 +77,8 @@ export const ColorRampField = ({ nbClass, }: ColorRampFieldProps) => { const locale = useLocale(); - const [state, dispatch] = useConfiguratorState(); + const [state, dispatch] = useConfiguratorState(isConfiguring); + const chartConfig = getChartConfig(state); const { palettes, defaultPalette } = useMemo(() => { const palettes = [...sequentialPalettes, ...divergingPalettes]; @@ -86,10 +89,9 @@ export const ColorRampField = ({ return { palettes, defaultPalette }; }, []); - const currentPaletteName = get( - state, - `chartConfig.fields.${field}.${path}` - ) as DivergingPaletteType | SequentialPaletteType; + const currentPaletteName = get(chartConfig, `fields.${field}.${path}`) as + | DivergingPaletteType + | SequentialPaletteType; const currentPalette = palettes.find((d) => d.value === currentPaletteName) || defaultPalette; diff --git a/app/configurator/components/chart-options-selector.tsx b/app/configurator/components/chart-options-selector.tsx index 173f15609..f9a58b82a 100644 --- a/app/configurator/components/chart-options-selector.tsx +++ b/app/configurator/components/chart-options-selector.tsx @@ -19,11 +19,11 @@ import { GenericField } from "@/config-types"; import { AnimationField, ChartConfig, - ChartType, ColorFieldType, ColorScaleType, ComponentType, ConfiguratorStateConfiguringChart, + getChartConfig, ImputationType, imputationTypes, isAnimationInConfig, @@ -81,7 +81,9 @@ export const ChartOptionsSelector = ({ }: { state: ConfiguratorStateConfiguringChart; }) => { - const { activeField, chartConfig, dataSet, dataSource } = state; + const chartConfig = getChartConfig(state); + const { dataSet, dataSource } = state; + const { activeField } = chartConfig; const locale = useLocale(); const [{ data: metadataData }] = useDataCubeMetadataQuery({ variables: { @@ -134,6 +136,7 @@ export const ChartOptionsSelector = ({ ) : ( @@ -150,25 +153,26 @@ export const ChartOptionsSelector = ({ type ActiveFieldSwitchProps = { state: ConfiguratorStateConfiguringChart; + chartConfig: ChartConfig; metadata: DataCubeMetadataWithHierarchies; observations: Observation[]; }; const ActiveFieldSwitch = (props: ActiveFieldSwitchProps) => { - const { state, metadata, observations } = props; + const { state, metadata, chartConfig, observations } = props; const { dimensions, measures } = metadata; - const activeField = state.activeField as EncodingFieldType | undefined; + const activeField = chartConfig.activeField as EncodingFieldType | undefined; if (!activeField) { return null; } - const chartSpec = getChartSpec(state.chartConfig); + const chartSpec = getChartSpec(chartConfig); // Animation field is a special field that is not part of the encodings, // but rather is selected from interactive filters menu. const animatable = - isAnimationInConfig(state.chartConfig) && + isAnimationInConfig(chartConfig) && chartSpec.interactiveFilters.includes("animation"); const baseEncodings = chartSpec.encodings; const encodings = animatable @@ -179,7 +183,7 @@ const ActiveFieldSwitch = (props: ActiveFieldSwitchProps) => { ) as EncodingSpec; const activeFieldComponentIri = getFieldComponentIri( - state.chartConfig.fields, + chartConfig.fields, activeField ); @@ -190,8 +194,8 @@ const ActiveFieldSwitch = (props: ActiveFieldSwitchProps) => { { type EncodingOptionsPanelProps = { encoding: EncodingSpec; state: ConfiguratorStateConfiguringChart; + chartConfig: ChartConfig; field: EncodingFieldType; - chartType: ChartType; component: DimensionMetadataFragment | undefined; dimensions: DimensionMetadataFragment[]; measures: DimensionMetadataFragment[]; @@ -216,12 +220,13 @@ const EncodingOptionsPanel = (props: EncodingOptionsPanelProps) => { encoding, state, field, - chartType, + chartConfig, component, dimensions, measures, observations, } = props; + const { chartType } = chartConfig; const fieldLabelHint: Record = { animation: t({ id: "controls.select.dimension", @@ -253,7 +258,7 @@ const EncodingOptionsPanel = (props: EncodingOptionsPanelProps) => { }), }; - const { fields } = state.chartConfig; + const { fields } = chartConfig; const otherFields = Object.keys(fields).filter( (f) => (fields as any)[f].hasOwnProperty("componentIri") && field !== f ); @@ -266,13 +271,13 @@ const EncodingOptionsPanel = (props: EncodingOptionsPanelProps) => { dimensionTypes: encoding.componentTypes, dimensions, measures, - }).map((dimension) => ({ - value: dimension.iri, - label: dimension.label, + }).map((d) => ({ + value: d.iri, + label: d.label, disabled: ((encoding.exclusive === undefined || encoding.exclusive === true) && - otherFieldsIris.includes(dimension.iri)) || - isStandardErrorDimension(dimension), + otherFieldsIris.includes(d.iri)) || + isStandardErrorDimension(d), })); }, [ dimensions, @@ -345,32 +350,31 @@ const EncodingOptionsPanel = (props: EncodingOptionsPanelProps) => { )} - {encoding.options?.imputation?.shouldShow( - state.chartConfig, - observations - ) && } + {encoding.options?.imputation?.shouldShow(chartConfig, observations) && ( + + )} {encoding.options?.calculation && get(fields, "segment") && ( )} {/* FIXME: should be generic or shouldn't be a field at all */} - {field === "baseLayer" && } + {field === "baseLayer" && ( + + )} {encoding.sorting && isDimensionSortable(component) && ( @@ -389,7 +393,7 @@ const EncodingOptionsPanel = (props: EncodingOptionsPanelProps) => { {encoding.options?.colorComponent && component && ( { { {fieldDimension && field === "animation" && - isAnimationInConfig(state.chartConfig) && - state.chartConfig.fields.animation && ( - + isAnimationInConfig(chartConfig) && + chartConfig.fields.animation && ( + )} ); @@ -633,6 +638,7 @@ const ChartFieldAnimation = ({ field }: { field: AnimationField }) => { const ChartFieldMultiFilter = ({ state, + chartConfig, component, encoding, field, @@ -640,6 +646,7 @@ const ChartFieldMultiFilter = ({ measures, }: { state: ConfiguratorStateConfiguringChart; + chartConfig: ChartConfig; component: DimensionMetadataFragment | undefined; encoding: EncodingSpec; field: string; @@ -647,13 +654,13 @@ const ChartFieldMultiFilter = ({ measures: DimensionMetadataFragment[]; }) => { const colorComponentIri = get( - state.chartConfig, + chartConfig, `fields.${field}.color.componentIri` ); const colorComponent = [...dimensions, ...measures].find( (d) => d.iri === colorComponentIri ); - const colorType = get(state.chartConfig, `fields.${field}.color.type`) as + const colorType = get(chartConfig, `fields.${field}.color.type`) as | ColorFieldType | undefined; @@ -797,12 +804,12 @@ const ChartFieldCalculation = (props: ChartFieldCalculationProps) => { }; const ChartFieldSorting = ({ - state, + chartConfig, field, encodingSortingOptions, disabled = false, }: { - state: ConfiguratorStateConfiguringChart; + chartConfig: ChartConfig; field: EncodingFieldType; encodingSortingOptions: EncodingSortingOption[]; disabled?: boolean; @@ -849,8 +856,8 @@ const ChartFieldSorting = ({ ); const activeSortingType = get( - state, - ["chartConfig", "fields", field, "sorting", "sortingType"], + chartConfig, + ["fields", field, "sorting", "sortingType"], DEFAULT_SORTING["sortingType"] ); @@ -859,8 +866,8 @@ const ChartFieldSorting = ({ (o) => o.sortingType === activeSortingType )?.sortingOrder; const activeSortingOrder = get( - state, - ["chartConfig", "fields", field, "sorting", "sortingOrder"], + chartConfig, + ["fields", field, "sorting", "sortingOrder"], sortingOrderOptions?.[0] ?? "asc" ); @@ -875,7 +882,7 @@ const ChartFieldSorting = ({ id="sort-by" label={getFieldLabel("sortBy")} options={encodingSortingOptions?.map((d) => { - const disabledState = d.getDisabledState?.(state.chartConfig); + const disabledState = d.getDisabledState?.(chartConfig); return { value: d.sortingType, @@ -898,11 +905,11 @@ const ChartFieldSorting = ({ {sortingOrderOptions && sortingOrderOptions.map((sortingOrder) => { const subType = get( - state, - ["chartConfig", "fields", "segment", "type"], + chartConfig, + ["fields", "segment", "type"], "" ); - const chartSubType = `${state.chartConfig.chartType}.${subType}`; + const chartSubType = `${chartConfig.chartType}.${subType}`; return ( { }; type ChartImputationProps = { - state: ConfiguratorStateConfiguringChart; + chartConfig: ChartConfig; }; const ChartImputation = (props: ChartImputationProps) => { - const { state } = props; + const { chartConfig } = props; const [, dispatch] = useConfiguratorState(); const getImputationTypeLabel = (type: ImputationType) => { switch (type) { @@ -1235,8 +1242,8 @@ const ChartImputation = (props: ChartImputationProps) => { ); const imputationType: ImputationType = get( - state, - ["chartConfig", "fields", "y", "imputationType"], + chartConfig, + ["fields", "y", "imputationType"], "none" ); @@ -1275,11 +1282,10 @@ const ChartImputation = (props: ChartImputationProps) => { }; const ChartMapBaseLayerSettings = ({ - state, + chartConfig, }: { - state: ConfiguratorStateConfiguringChart; + chartConfig: MapConfig; }) => { - const chartConfig = state.chartConfig as MapConfig; const locale = useLocale(); const [_, dispatch] = useConfiguratorState(isConfiguring); diff --git a/app/configurator/components/configurator.tsx b/app/configurator/components/configurator.tsx index 11a9c18f7..275efe986 100644 --- a/app/configurator/components/configurator.tsx +++ b/app/configurator/components/configurator.tsx @@ -9,7 +9,7 @@ import { ChartPanelConfigurator } from "@/components/chart-panel"; import { ChartPreview } from "@/components/chart-preview"; import { HEADER_HEIGHT } from "@/components/header"; import { Loading } from "@/components/hint"; -import { useConfiguratorState } from "@/configurator"; +import { getChartConfig, useConfiguratorState } from "@/configurator"; import { ChartAnnotationsSelector } from "@/configurator/components/chart-annotations-selector"; import { ChartConfigurator } from "@/configurator/components/chart-configurator"; import { ChartOptionsSelector } from "@/configurator/components/chart-options-selector"; @@ -109,6 +109,8 @@ const ConfigureChartStep = () => { return null; } + const chartConfig = getChartConfig(state); + return ( <> { Back to preview - {state.chartConfig.chartType === "table" ? ( + {chartConfig.chartType === "table" ? ( ) : ( @@ -140,7 +142,7 @@ const ConfigureChartStep = () => { @@ -157,7 +159,7 @@ const ConfigureChartStep = () => { Back to main - {isAnnotationField(state.activeField) ? ( + {isAnnotationField(chartConfig.activeField) ? ( ) : ( diff --git a/app/configurator/components/field.tsx b/app/configurator/components/field.tsx index 31fa7b822..8548dd0b0 100644 --- a/app/configurator/components/field.tsx +++ b/app/configurator/components/field.tsx @@ -30,6 +30,7 @@ import { } from "@/components/form"; import SelectTree from "@/components/select-tree"; import useDisclosure from "@/components/use-disclosure"; +import { getChartConfig } from "@/config-types"; import { ColorPickerMenu } from "@/configurator/components/chart-controls/color-picker"; import { AnnotatorTab, @@ -548,8 +549,9 @@ export const MetaInputField = ({ const useMultiFilterColorPicker = (value: string) => { const [state, dispatch] = useConfiguratorState(isConfiguring); + const chartConfig = getChartConfig(state); const { dimensionIri, colorConfigPath } = useMultiFilterContext(); - const { activeField, chartConfig } = state; + const { activeField } = chartConfig; const onChange = useCallback( (color: string) => { if (activeField) { @@ -642,7 +644,8 @@ export const ColorPickerField = ({ disabled?: boolean; }) => { const locale = useLocale(); - const [state, dispatch] = useConfiguratorState(); + const [state, dispatch] = useConfiguratorState(isConfiguring); + const chartConfig = getChartConfig(state); const updateColor = useCallback( (value: string) => @@ -658,11 +661,7 @@ export const ColorPickerField = ({ [locale, dispatch, field, path] ); - if (state.state !== "CONFIGURING_CHART") { - return null; - } - - const color = get(state, `chartConfig.fields["${field}"].${path}`); + const color = get(chartConfig, `fields["${field}"].${path}`); return ( { }; const getColorConfig = ( - config: ConfiguratorState, + chartConfig: ChartConfig, colorConfigPath: string | undefined ) => { - if (!config.activeField) { + if (!chartConfig.activeField) { return; } const path = colorConfigPath - ? [config.activeField, colorConfigPath] - : [config.activeField]; + ? [chartConfig.activeField, colorConfigPath] + : [chartConfig.activeField]; - return get(config.chartConfig.fields, path) as - | GenericSegmentField - | undefined; + return get(chartConfig.fields, path) as GenericSegmentField | undefined; }; const FilterControls = ({ @@ -227,9 +227,10 @@ const MultiFilterContent = ({ tree: HierarchyValue[]; }) => { const [config, dispatch] = useConfiguratorState(isConfiguring); + const chartConfig = getChartConfig(config); const { dimensionIri, activeKeys, allValues, colorConfigPath } = useMultiFilterContext(); - const rawValues = config.chartConfig.filters[dimensionIri]; + const rawValues = chartConfig.filters[dimensionIri]; const classes = useStyles(); @@ -340,8 +341,8 @@ const MultiFilterContent = ({ ); const colorConfig = useMemo(() => { - return getColorConfig(config, colorConfigPath); - }, [config, colorConfigPath]); + return getColorConfig(chartConfig, colorConfigPath); + }, [chartConfig, colorConfigPath]); const hasColorMapping = useMemo(() => { return ( @@ -358,7 +359,7 @@ const MultiFilterContent = ({ - {config.activeField === "segment" ? ( + {chartConfig.activeField === "segment" ? ( } @@ -899,7 +900,9 @@ export const DimensionValuesMultiFilter = ({ field?: string; }) => { const locale = useLocale(); - const [{ dataSource, chartConfig }] = useConfiguratorState(isConfiguring); + const [state] = useConfiguratorState(isConfiguring); + const { dataSource } = state; + const chartConfig = getChartConfig(state); const [{ data }] = useDimensionValuesQuery({ variables: { diff --git a/app/configurator/config-form.tsx b/app/configurator/config-form.tsx index 99149d263..461ed1ce5 100644 --- a/app/configurator/config-form.tsx +++ b/app/configurator/config-form.tsx @@ -14,7 +14,7 @@ import { useClient } from "urql"; import { getFieldComponentIri } from "@/charts"; import { EncodingFieldType } from "@/charts/chart-config-ui-options"; -import { ChartConfig, ChartType } from "@/config-types"; +import { ChartConfig, ChartType, getChartConfig } from "@/config-types"; import { getChartOptionField, getFilterValue, @@ -167,8 +167,8 @@ export const useChartFieldField = ({ let value: string | undefined; if (state.state === "CONFIGURING_CHART") { - value = - getFieldComponentIri(state.chartConfig.fields, field) ?? FIELD_VALUE_NONE; + const chartConfig = getChartConfig(state); + value = getFieldComponentIri(chartConfig.fields, field) ?? FIELD_VALUE_NONE; } return { @@ -210,7 +210,11 @@ export const useChartOptionSelectField = ( let value: V | undefined; if (state.state === "CONFIGURING_CHART") { - value = get(state, `chartConfig.fields.${field}.${path}`, FIELD_VALUE_NONE); + value = get( + getChartConfig(state), + `fields.${field}.${path}`, + FIELD_VALUE_NONE + ); } return { @@ -370,7 +374,8 @@ export const useActiveFieldField = ({ }): FieldProps & { onClick: (x: string) => void; } => { - const [state, dispatch] = useConfiguratorState(); + const [state, dispatch] = useConfiguratorState(isConfiguring); + const chartConfig = getChartConfig(state); const onClick = useCallback(() => { dispatch({ @@ -379,7 +384,7 @@ export const useActiveFieldField = ({ }); }, [dispatch, value]); - const checked = state.activeField === value; + const checked = chartConfig.activeField === value; return { value, @@ -395,6 +400,7 @@ export const useChartType = (): { } => { const locale = useLocale(); const [state, dispatch] = useConfiguratorState(); + const chartConfig = getChartConfig(state); const onChange = useEvent((chartType: ChartType) => { dispatch({ type: "CHART_TYPE_CHANGED", @@ -405,10 +411,7 @@ export const useChartType = (): { }); }); - const value = - state.state === "CONFIGURING_CHART" - ? get(state, "chartConfig.chartType") - : ""; + const value = get(chartConfig, "chartType"); return { onChange, @@ -447,8 +450,9 @@ export const useSingleFilterSelect = ({ let value: string | undefined; if (state.state === "CONFIGURING_CHART") { + const chartConfig = getChartConfig(state); value = get( - state.chartConfig, + chartConfig, ["filters", dimensionIri, "value"], FIELD_VALUE_NONE ); @@ -484,7 +488,7 @@ export const useSingleFilterField = ({ const stateValue = state.state === "CONFIGURING_CHART" - ? get(state.chartConfig, ["filters", dimensionIri, "value"], "") + ? get(getChartConfig(state), ["filters", dimensionIri, "value"], "") : ""; const checked = stateValue === value; diff --git a/app/configurator/configurator-state.spec.tsx b/app/configurator/configurator-state.spec.tsx index d2f2f579f..eab5a5345 100644 --- a/app/configurator/configurator-state.spec.tsx +++ b/app/configurator/configurator-state.spec.tsx @@ -792,30 +792,31 @@ describe("colorMapping", () => { type: "sparql", url: "fakeUrl", }, - chartConfig: { - chartType: "column", - fields: { - y: { - componentIri: "measure", + chartConfigs: [ + { + key: "abc", + chartType: "column", + fields: { + y: { + componentIri: "measure", + }, }, + filters: {}, }, - filters: {}, - }, + ], + activeChartKey: "abc", } as ConfiguratorStateConfiguringChart; - handleChartFieldChanged( - state as unknown as ConfiguratorStateConfiguringChart, - { - type: "CHART_FIELD_CHANGED", - value: { - locale: "en", - field: "segment", - componentIri: "newAreaLayerColorIri", - }, - } - ); + handleChartFieldChanged(state, { + type: "CHART_FIELD_CHANGED", + value: { + locale: "en", + field: "segment", + componentIri: "newAreaLayerColorIri", + }, + }); - const chartConfig = state.chartConfig as ColumnConfig; + const chartConfig = state.chartConfigs[0] as ColumnConfig; expect(chartConfig.fields.segment?.componentIri === "newAreaLayerColorIri"); expect(chartConfig.fields.segment?.palette === "dimension"); @@ -834,40 +835,43 @@ describe("handleChartFieldChanged", () => { type: "sparql", url: "fakeUrl", }, - chartConfig: { - chartType: "map", - fields: { - symbolLayer: { - componentIri: "symbolLayerIri", - color: { - type: "categorical", - componentIri: "symbolLayerColorIri", - palette: "oranges", - colorMapping: { - red: "green", - green: "blue", - blue: "red", + chartConfigs: [ + { + key: "cba", + chartType: "map", + fields: { + symbolLayer: { + componentIri: "symbolLayerIri", + color: { + type: "categorical", + componentIri: "symbolLayerColorIri", + palette: "oranges", + colorMapping: { + red: "green", + green: "blue", + blue: "red", + }, }, }, }, + filters: {}, }, - filters: {}, - }, - }; + ], + activeChartKey: "cba", + } as unknown as ConfiguratorStateConfiguringChart; - handleChartFieldChanged( - state as unknown as ConfiguratorStateConfiguringChart, - { - type: "CHART_FIELD_CHANGED", - value: { - locale: "en", - field: "symbolLayer", - componentIri: "symbolLayerIri", - }, - } - ); + handleChartFieldChanged(state, { + type: "CHART_FIELD_CHANGED", + value: { + locale: "en", + field: "symbolLayer", + componentIri: "symbolLayerIri", + }, + }); - expect(state.chartConfig.fields.symbolLayer.color).toBeDefined(); + expect( + (state.chartConfigs[0] as any).fields.symbolLayer.color + ).toBeDefined(); }); }); @@ -880,22 +884,26 @@ describe("handleChartOptionChanged", () => { type: "sparql", url: "fakeUrl", }, - chartConfig: { - chartType: "map", - fields: { - areaLayer: { - componentIri: "areaLayerIri", - color: { - type: "numerical", - componentIri: "areaLayerColorIri", - palette: "oranges", - scaleType: "continuous", - interpolationType: "linear", + chartConfigs: [ + { + key: "bac", + chartType: "map", + fields: { + areaLayer: { + componentIri: "areaLayerIri", + color: { + type: "numerical", + componentIri: "areaLayerColorIri", + palette: "oranges", + scaleType: "continuous", + interpolationType: "linear", + }, }, }, + filters: {}, }, - filters: {}, - }, + ], + activeChartKey: "bac", } as unknown as ConfiguratorStateConfiguringChart; handleChartOptionChanged(state, { @@ -909,7 +917,7 @@ describe("handleChartOptionChanged", () => { }); expect( - (state.chartConfig as any).fields.areaLayer.color.nbClass + (state.chartConfigs[0] as any).fields.areaLayer.color.nbClass ).toBeTruthy(); }); @@ -921,33 +929,37 @@ describe("handleChartOptionChanged", () => { type: "sparql", url: "fakeUrl", }, - chartConfig: { - chartType: "map", - fields: { - areaLayer: { - componentIri: "areaLayerIri", - color: { - type: "categorical", - componentIri: "areaLayerColorIri", - palette: "dimension", - colorMapping: { - red: "green", - green: "blue", - blue: "red", + chartConfigs: [ + { + key: "cab", + chartType: "map", + fields: { + areaLayer: { + componentIri: "areaLayerIri", + color: { + type: "categorical", + componentIri: "areaLayerColorIri", + palette: "dimension", + colorMapping: { + red: "green", + green: "blue", + blue: "red", + }, }, }, }, - }, - filters: { - areaLayerColorIri: { - type: "multi", - values: { - red: true, - green: true, + filters: { + areaLayerColorIri: { + type: "multi", + values: { + red: true, + green: true, + }, }, }, }, - }, + ], + activeChartKey: "cab", } as unknown as ConfiguratorStateConfiguringChart; handleChartOptionChanged(state, { @@ -960,7 +972,7 @@ describe("handleChartOptionChanged", () => { }, }); - expect(Object.keys(state.chartConfig.filters)).not.toContain( + expect(Object.keys(state.chartConfigs[0].filters)).not.toContain( "areaLayerColorIri" ); }); diff --git a/app/configurator/configurator-state.tsx b/app/configurator/configurator-state.tsx index 0fec8ed89..f2358825c 100644 --- a/app/configurator/configurator-state.tsx +++ b/app/configurator/configurator-state.tsx @@ -1,4 +1,4 @@ -import { current, produce } from "immer"; +import produce, { current } from "immer"; import get from "lodash/get"; import pickBy from "lodash/pickBy"; import setWith from "lodash/setWith"; @@ -46,6 +46,7 @@ import { ImputationType, InteractiveFiltersConfig, decodeConfiguratorState, + getChartConfig, isAreaConfig, isColorFieldInConfig, isTableConfig, @@ -79,7 +80,10 @@ import { useDataSourceStore, } from "@/stores/data-source"; import { createConfig, fetchChartConfig } from "@/utils/chart-config/api"; -import { migrateChartConfig } from "@/utils/chart-config/versioning"; +import { + CONFIGURATOR_STATE_VERSION, + migrateConfiguratorState, +} from "@/utils/chart-config/versioning"; import { createChartId } from "@/utils/create-chart-id"; import { unreachableError } from "@/utils/unreachable"; @@ -281,6 +285,16 @@ export type ConfiguratorStateAction = | { type: "PUBLISHED"; value: string; + } + | { + type: "CHART_CONFIG_ADD"; + value: { + chartConfig: ChartConfig; + }; + } + | { + type: "SWITCH_ACTIVE_CHART"; + value: string; }; const LOCALSTORAGE_PREFIX = "vizualize-configurator-state"; @@ -297,17 +311,18 @@ const getStateWithCurrentDataSource = (state: ConfiguratorState) => { }; const INITIAL_STATE: ConfiguratorState = { + version: CONFIGURATOR_STATE_VERSION, state: "INITIAL", dataSet: undefined, dataSource: DEFAULT_DATA_SOURCE, - activeField: undefined, }; const emptyState: ConfiguratorStateSelectingDataSet = { + version: CONFIGURATOR_STATE_VERSION, state: "SELECTING_DATASET", dataSet: undefined, dataSource: DEFAULT_DATA_SOURCE, - chartConfig: undefined, + chartConfigs: undefined, meta: { title: { de: "", @@ -322,7 +337,7 @@ const emptyState: ConfiguratorStateSelectingDataSet = { en: "", }, }, - activeField: undefined, + activeChartKey: undefined, }; const getCachedMetadata = ( @@ -362,7 +377,7 @@ export const getFilterValue = ( dimensionIri: string ): FilterValue | undefined => { return state.state !== "INITIAL" && state.state !== "SELECTING_DATASET" - ? state.chartConfig.filters[dimensionIri] + ? getChartConfig(state).filters[dimensionIri] : undefined; }; @@ -373,28 +388,34 @@ export const moveFilterField = produce( // https://262.ecma-international.org/6.0/#sec-ordinary-object-internal-methods-and-internal-slots-ownpropertykeys const keys = Object.getOwnPropertyNames(chartConfig.filters); const fieldIndex = Object.keys(chartConfig.filters).indexOf(dimensionIri); + if (fieldIndex === 0 && delta === -1) { return; } + if (fieldIndex === keys.length - 1 && delta === 1) { return; } + if (fieldIndex === -1 && delta !== -1) { return; } + const replacedIndex = fieldIndex === -1 ? keys.length - 1 : fieldIndex + delta; const replaced = keys[replacedIndex]; keys[replacedIndex] = dimensionIri; + if (fieldIndex === -1) { keys.push(replaced); } else { keys[fieldIndex] = replaced; } + chartConfig.filters = Object.fromEntries( keys.map((k) => [ k, - chartConfig.filters[k] || { type: "single", value: possibleValues[0] }, + chartConfig.filters[k] ?? { type: "single", value: possibleValues[0] }, ]) ); } @@ -573,29 +594,27 @@ const transitionStepNext = ( measures: dataSetMetadata.measures, }); - const chartConfig = deriveFiltersFromFields( - getInitialConfig({ - chartType: possibleChartTypes[0], - dimensions: dataSetMetadata.dimensions, - measures: dataSetMetadata.measures, - }), - dataSetMetadata.dimensions - ); + const chartConfig = getInitialConfig({ + chartType: possibleChartTypes[0], + dimensions: dataSetMetadata.dimensions, + measures: dataSetMetadata.measures, + }); + deriveFiltersFromFields(chartConfig, dataSetMetadata.dimensions); return { + version: CONFIGURATOR_STATE_VERSION, state: "CONFIGURING_CHART", dataSet: draft.dataSet, dataSource: draft.dataSource, meta: draft.meta, - activeField: undefined, - chartConfig, + chartConfigs: [chartConfig], + activeChartKey: chartConfig.key, }; } break; case "CONFIGURING_CHART": return { ...draft, - activeField: undefined, state: "PUBLISHING", }; @@ -605,6 +624,7 @@ const transitionStepNext = ( default: throw unreachableError(draft); } + return draft; }; @@ -638,19 +658,18 @@ const transitionStepPrevious = ( case "SELECTING_DATASET": return { ...draft, - activeField: undefined, - chartConfig: undefined, + chartConfigs: undefined, + activeChartKey: undefined, state: stepTo, }; case "CONFIGURING_CHART": return { ...draft, - activeField: undefined, state: stepTo, }; - default: - return draft; } + + return draft; }; // FIXME: should by handled better, as color is a subfield and not actual field. @@ -700,11 +719,11 @@ export const getChartOptionField = ( path: string, defaultValue: string | boolean = "" ) => { + const chartConfig = getChartConfig(state); + return get( - state, - field === null - ? `chartConfig.${path}` - : `chartConfig.fields["${field}"].${path}`, + chartConfig, + field === null ? path : `fields["${field}"].${path}`, defaultValue ); }; @@ -717,13 +736,14 @@ export const handleChartFieldChanged = ( return draft; } + const chartConfig = getChartConfig(draft); const { locale, field, componentIri, selectedValues: actionSelectedValues, } = action.value; - const f = get(draft.chartConfig.fields, field); + const f = get(chartConfig.fields, field); const { dimensions = [], measures = [] } = getCachedMetadata(draft, locale) ?? {}; const components = [...dimensions, ...measures]; @@ -732,12 +752,12 @@ export const handleChartFieldChanged = ( if (f) { // Reset field properties, excluding componentIri. - (draft.chartConfig.fields as GenericFields)[field] = { componentIri }; + (chartConfig.fields as GenericFields)[field] = { componentIri }; } - const sideEffect = getChartFieldChangeSideEffect(draft.chartConfig, field); + const sideEffect = getChartFieldChangeSideEffect(chartConfig, field); sideEffect?.(componentIri, { - draft, + chartConfig, dimensions, measures, initializing: !f, @@ -745,18 +765,28 @@ export const handleChartFieldChanged = ( }); // Remove the component from interactive data filters. - if (draft.chartConfig.interactiveFiltersConfig?.dataFilters) { + if (chartConfig.interactiveFiltersConfig?.dataFilters) { const componentIris = - draft.chartConfig.interactiveFiltersConfig.dataFilters.componentIris.filter( + chartConfig.interactiveFiltersConfig.dataFilters.componentIris.filter( (d) => d !== componentIri ); const active = componentIris.length > 0; - draft.chartConfig.interactiveFiltersConfig.dataFilters = { + chartConfig.interactiveFiltersConfig.dataFilters = { active, componentIris, }; } - draft.chartConfig = deriveFiltersFromFields(draft.chartConfig, dimensions); + + const newConfig = deriveFiltersFromFields(chartConfig, dimensions); + + for (const k in chartConfig) { + if (chartConfig.hasOwnProperty(k)) { + // @ts-ignore + delete chartConfig[k]; + } + } + + Object.assign(chartConfig, newConfig); return draft; }; @@ -767,27 +797,25 @@ export const handleChartOptionChanged = ( ) => { if (draft.state === "CONFIGURING_CHART") { const { locale, path, field, value } = action.value; - const updatePath = - field === null - ? `chartConfig.${path}` - : `chartConfig.fields["${field}"].${path}`; + const chartConfig = getChartConfig(draft); + const updatePath = field === null ? path : `fields["${field}"].${path}`; const { dimensions = [], measures = [] } = getCachedMetadata(draft, locale) ?? {}; if (field) { const sideEffect = getChartFieldOptionChangeSideEffect( - draft.chartConfig, + chartConfig, field, path ); - sideEffect?.(value, { draft, dimensions, measures }); + sideEffect?.(value, { chartConfig, dimensions, measures }); } if (value === FIELD_VALUE_NONE) { - unset(draft, updatePath); + unset(chartConfig, updatePath); } - setWith(draft, updatePath, value, Object); + setWith(chartConfig, updatePath, value, Object); } return draft; @@ -803,14 +831,15 @@ export const updateColorMapping = ( if (draft.state === "CONFIGURING_CHART") { const { field, colorConfigPath, dimensionIri, values, random } = action.value; + const chartConfig = getChartConfig(draft); const path = colorConfigPath ? ["fields", field, colorConfigPath] : ["fields", field]; let colorMapping: ColorMapping | undefined; - if (isTableConfig(draft.chartConfig)) { + if (isTableConfig(chartConfig)) { const fieldValue: ColumnStyleCategory | undefined = get( - draft.chartConfig, + chartConfig, path ); @@ -823,7 +852,7 @@ export const updateColorMapping = ( } } else { const fieldValue: (GenericField & { palette: string }) | undefined = get( - draft.chartConfig, + chartConfig, path ); @@ -837,7 +866,7 @@ export const updateColorMapping = ( } if (colorMapping) { - setWith(draft.chartConfig, path.concat("colorMapping"), colorMapping); + setWith(chartConfig, path.concat("colorMapping"), colorMapping, Object); } } @@ -852,12 +881,8 @@ const handleInteractiveFilterChanged = ( > ) => { if (draft.state === "CONFIGURING_CHART") { - setWith( - draft, - "chartConfig.interactiveFiltersConfig", - action.value, - Object - ); + const chartConfig = getChartConfig(draft); + setWith(chartConfig, "interactiveFiltersConfig", action.value, Object); } return draft; @@ -888,18 +913,22 @@ const reducer: Reducer = ( if (metadata) { const { dimensions, measures } = metadata; - const previousConfig = current(draft.chartConfig); - draft.chartConfig = getChartConfigAdjustedToChartType({ - chartConfig: previousConfig, + const previousConfig = getChartConfig(draft); + const newConfig = getChartConfigAdjustedToChartType({ + chartConfig: current(previousConfig), newChartType: chartType, dimensions, measures, }); - draft.activeField = undefined; - draft.chartConfig = deriveFiltersFromFields( - draft.chartConfig, - metadata.dimensions - ); + + for (const k in previousConfig) { + if (previousConfig.hasOwnProperty(k)) { + // @ts-ignore + delete previousConfig[k]; + } + } + + Object.assign(previousConfig, newConfig); } } @@ -907,8 +936,10 @@ const reducer: Reducer = ( case "ACTIVE_FIELD_CHANGED": if (draft.state === "CONFIGURING_CHART") { - draft.activeField = action.value; + const chartConfig = getChartConfig(draft); + chartConfig.activeField = action.value; } + return draft; case "CHART_FIELD_CHANGED": @@ -916,30 +947,20 @@ const reducer: Reducer = ( case "CHART_FIELD_DELETED": if (draft.state === "CONFIGURING_CHART") { - delete (draft.chartConfig.fields as GenericFields)[action.value.field]; + const chartConfig = getChartConfig(draft); + delete (chartConfig.fields as GenericFields)[action.value.field]; const metadata = getCachedMetadata(draft, action.value.locale); const dimensions = metadata?.dimensions ?? []; - draft.chartConfig = deriveFiltersFromFields( - draft.chartConfig, - dimensions - ); + deriveFiltersFromFields(chartConfig, dimensions); if ( action.value.field === "segment" && - draft.chartConfig.interactiveFiltersConfig + chartConfig.interactiveFiltersConfig ) { - draft.chartConfig = { - ...draft.chartConfig, - interactiveFiltersConfig: { - ...draft.chartConfig.interactiveFiltersConfig, - calculation: { - active: false, - type: "identity", - }, - }, - }; + chartConfig.interactiveFiltersConfig.calculation.active = false; + chartConfig.interactiveFiltersConfig.calculation.type = "identity"; } } @@ -950,9 +971,10 @@ const reducer: Reducer = ( case "CHART_PALETTE_CHANGED": if (draft.state === "CONFIGURING_CHART") { + const chartConfig = getChartConfig(draft); setWith( - draft, - `chartConfig.fields["${action.value.field}"].${ + chartConfig, + `fields["${action.value.field}"].${ action.value.colorConfigPath ? `${action.value.colorConfigPath}.` : "" @@ -961,8 +983,8 @@ const reducer: Reducer = ( Object ); setWith( - draft, - `chartConfig.fields["${action.value.field}"].${ + chartConfig, + `fields["${action.value.field}"].${ action.value.colorConfigPath ? `${action.value.colorConfigPath}.` : "" @@ -971,12 +993,15 @@ const reducer: Reducer = ( Object ); } + return draft; + case "CHART_PALETTE_RESET": if (draft.state === "CONFIGURING_CHART") { + const chartConfig = getChartConfig(draft); setWith( - draft, - `chartConfig.fields["${action.value.field}"].${ + chartConfig, + `fields["${action.value.field}"].${ action.value.colorConfigPath ? `${action.value.colorConfigPath}.` : "" @@ -985,13 +1010,15 @@ const reducer: Reducer = ( Object ); } + return draft; case "CHART_COLOR_CHANGED": if (draft.state === "CONFIGURING_CHART") { + const chartConfig = getChartConfig(draft); setWith( - draft, - `chartConfig.fields["${action.value.field}"].${ + chartConfig, + `fields["${action.value.field}"].${ action.value.colorConfigPath ? `${action.value.colorConfigPath}.` : "" @@ -1004,8 +1031,15 @@ const reducer: Reducer = ( case "CHART_DESCRIPTION_CHANGED": if (draft.state === "CONFIGURING_CHART") { - setWith(draft, `meta.${action.value.path}`, action.value.value, Object); + const chartConfig = getChartConfig(draft); + setWith( + chartConfig, + `meta.${action.value.path}`, + action.value.value, + Object + ); } + return draft; case "INTERACTIVE_FILTER_CHANGED": @@ -1013,9 +1047,21 @@ const reducer: Reducer = ( case "CHART_CONFIG_REPLACED": if (draft.state === "CONFIGURING_CHART") { - draft.chartConfig = deriveFiltersFromFields( - action.value.chartConfig, - action.value.dataSetMetadata.dimensions + const chartConfig = getChartConfig(draft); + + for (const k in chartConfig) { + if (chartConfig.hasOwnProperty(k)) { + // @ts-ignore + delete chartConfig[k]; + } + } + + Object.assign( + chartConfig, + deriveFiltersFromFields( + action.value.chartConfig, + action.value.dataSetMetadata.dimensions + ) ); } @@ -1024,29 +1070,33 @@ const reducer: Reducer = ( case "CHART_CONFIG_FILTER_SET_SINGLE": if (draft.state === "CONFIGURING_CHART") { const { dimensionIri, value } = action.value; + const chartConfig = getChartConfig(draft); if (value === FIELD_VALUE_NONE) { - delete draft.chartConfig.filters[dimensionIri]; + delete chartConfig.filters[dimensionIri]; } else { - draft.chartConfig.filters[dimensionIri] = { + chartConfig.filters[dimensionIri] = { type: "single", value, }; } } + return draft; case "CHART_CONFIG_FILTER_REMOVE_SINGLE": if (draft.state === "CONFIGURING_CHART") { const { dimensionIri } = action.value; - delete draft.chartConfig.filters[dimensionIri]; + const chartConfig = getChartConfig(draft); + delete chartConfig.filters[dimensionIri]; const newIFConfig = toggleInteractiveFilterDataDimension( - draft.chartConfig.interactiveFiltersConfig, + chartConfig.interactiveFiltersConfig, dimensionIri, false ); - draft.chartConfig.interactiveFiltersConfig = newIFConfig; + chartConfig.interactiveFiltersConfig = newIFConfig; } + return draft; case "CHART_CONFIG_UPDATE_COLOR_MAPPING": @@ -1055,14 +1105,17 @@ const reducer: Reducer = ( case "CHART_CONFIG_FILTER_SET_MULTI": if (draft.state === "CONFIGURING_CHART") { const { dimensionIri, values } = action.value; - draft.chartConfig.filters[dimensionIri] = makeMultiFilter(values); + const chartConfig = getChartConfig(draft); + chartConfig.filters[dimensionIri] = makeMultiFilter(values); } + return draft; case "CHART_CONFIG_FILTER_ADD_MULTI": if (draft.state === "CONFIGURING_CHART") { const { dimensionIri, values, allValues } = action.value; - const f = draft.chartConfig.filters[dimensionIri]; + const chartConfig = getChartConfig(draft); + const f = chartConfig.filters[dimensionIri]; const newFilter = makeMultiFilter(values); if (f && f.type === "multi") { f.values = { @@ -1071,18 +1124,20 @@ const reducer: Reducer = ( }; // If all values are selected, we remove the filter again! if (allValues.every((v) => v in f.values)) { - delete draft.chartConfig.filters[dimensionIri]; + delete chartConfig.filters[dimensionIri]; } } else { - draft.chartConfig.filters[dimensionIri] = newFilter; + chartConfig.filters[dimensionIri] = newFilter; } } + return draft; case "CHART_CONFIG_FILTER_REMOVE_MULTI": if (draft.state === "CONFIGURING_CHART") { const { dimensionIri, values, allValues } = action.value; - const f = draft.chartConfig.filters[dimensionIri]; + const chartConfig = getChartConfig(draft); + const f = chartConfig.filters[dimensionIri]; if (f && f.type === "multi" && Object.keys(f.values).length > 0) { // If there are existing object keys, we just remove the current one @@ -1101,45 +1156,51 @@ const reducer: Reducer = ( }, {} ); - draft.chartConfig.filters[dimensionIri] = { + chartConfig.filters[dimensionIri] = { type: "multi", values: updatedValues, }; } } + return draft; case "CHART_CONFIG_FILTER_RESET_MULTI": case "CHART_CONFIG_FILTER_RESET_RANGE": if (draft.state === "CONFIGURING_CHART") { const { dimensionIri } = action.value; - delete draft.chartConfig.filters[dimensionIri]; + const chartConfig = getChartConfig(draft); + delete chartConfig.filters[dimensionIri]; } + return draft; case "CHART_CONFIG_FILTER_SET_NONE_MULTI": if (draft.state === "CONFIGURING_CHART") { const { dimensionIri } = action.value; - draft.chartConfig.filters[dimensionIri] = { + const chartConfig = getChartConfig(draft); + chartConfig.filters[dimensionIri] = { type: "multi", values: {}, }; } + return draft; case "CHART_CONFIG_FILTER_SET_RANGE": if (draft.state === "CONFIGURING_CHART") { const { dimensionIri, from, to } = action.value; - draft.chartConfig.filters[dimensionIri] = { + const chartConfig = getChartConfig(draft); + chartConfig.filters[dimensionIri] = { type: "range", from, to, }; - if (draft.chartConfig.interactiveFiltersConfig) { - draft.chartConfig.interactiveFiltersConfig.timeRange = { + if (chartConfig.interactiveFiltersConfig) { + chartConfig.interactiveFiltersConfig.timeRange = { componentIri: dimensionIri, - active: draft.chartConfig.interactiveFiltersConfig.timeRange.active, + active: chartConfig.interactiveFiltersConfig.timeRange.active, presets: { type: "range", from, @@ -1148,19 +1209,23 @@ const reducer: Reducer = ( }; } } + return draft; case "CHART_CONFIG_FILTERS_UPDATE": if (draft.state === "CONFIGURING_CHART") { const { filters } = action.value; - draft.chartConfig.filters = filters; + const chartConfig = getChartConfig(draft); + chartConfig.filters = filters; } + return draft; case "IMPUTATION_TYPE_CHANGED": if (draft.state === "CONFIGURING_CHART") { - if (isAreaConfig(draft.chartConfig)) { - draft.chartConfig.fields.y.imputationType = action.value.type; + const chartConfig = getChartConfig(draft); + if (isAreaConfig(chartConfig)) { + chartConfig.fields.y.imputationType = action.value.type; } } @@ -1183,6 +1248,21 @@ const reducer: Reducer = ( case "PUBLISHED": return draft; + case "CHART_CONFIG_ADD": + if (draft.state === "CONFIGURING_CHART") { + draft.chartConfigs.push(action.value.chartConfig); + draft.activeChartKey = action.value.chartConfig.key; + } + + return draft; + + case "SWITCH_ACTIVE_CHART": + if (draft.state === "CONFIGURING_CHART") { + draft.activeChartKey = action.value; + } + + return draft; + default: throw unreachableError(action); } @@ -1201,22 +1281,7 @@ export const initChartStateFromChart = async ( const config = await fetchChartConfig(from); if (config?.data) { - const { - dataSet, - dataSource = DEFAULT_DATA_SOURCE, - meta, - chartConfig, - } = config.data; - const migratedChartConfig = migrateChartConfig(chartConfig); - - return { - state: "CONFIGURING_CHART", - dataSet, - dataSource, - meta, - chartConfig: migratedChartConfig, - activeField: undefined, - }; + return migrateConfiguratorState(config.data); } }; @@ -1271,12 +1336,8 @@ export const initChartStateFromLocalStorage = async ( let parsedState; try { const rawParsedState = JSON.parse(storedState); - const chartConfig = rawParsedState.chartConfig; - const migratedChartConfig = migrateChartConfig(chartConfig); - parsedState = decodeConfiguratorState({ - ...rawParsedState, - chartConfig: migratedChartConfig, - }); + const migratedState = migrateConfiguratorState(rawParsedState); + parsedState = decodeConfiguratorState(migratedState); } catch (e) { console.error("Error while parsing stored state", e); // Ignore errors since we are returning undefined and removing bad state from localStorage @@ -1336,9 +1397,7 @@ const ConfiguratorStateProviderInternal = ({ } } else { newChartState = await initChartStateFromLocalStorage(chartId); - if (!newChartState) { - if (allowDefaultRedirect) replace(`/create/new`); - } + if (!newChartState && allowDefaultRedirect) replace(`/create/new`); } stateToInitialize = newChartState || stateToInitialize; @@ -1390,18 +1449,20 @@ const ConfiguratorStateProviderInternal = ({ try { const result = await createConfig({ ...state, - chartConfig: { - ...state.chartConfig, - // Ensure that the filters are in the correct order, as JSON - // does not guarantee order (and we need this as interactive - // filters are dependent on the order of the filters). - filters: Object.fromEntries( - Object.entries(state.chartConfig.filters).map( - ([k, v], i) => { - return [k, { ...v, position: i }]; - } - ) - ), + chartConfigs: { + ...state.chartConfigs.map((d) => { + return { + ...d, + // Ensure that the filters are in the correct order, as JSON + // does not guarantee order (and we need this as interactive + // filters are dependent on the order of the filters). + filters: Object.fromEntries( + Object.entries(d.filters).map(([k, v], i) => { + return [k, { ...v, position: i }]; + }) + ), + }; + }), }, }); diff --git a/app/configurator/interactive-filters/interactive-filters-config-state.tsx b/app/configurator/interactive-filters/interactive-filters-config-state.tsx index 557adb16d..47ce3fee5 100644 --- a/app/configurator/interactive-filters/interactive-filters-config-state.tsx +++ b/app/configurator/interactive-filters/interactive-filters-config-state.tsx @@ -2,7 +2,7 @@ import produce from "immer"; import get from "lodash/get"; import { ChangeEvent, useCallback } from "react"; -import { InteractiveFiltersConfig } from "@/config-types"; +import { InteractiveFiltersConfig, getChartConfig } from "@/config-types"; import { isConfiguring, useConfiguratorState, @@ -12,27 +12,22 @@ import useEvent from "@/utils/use-event"; export const useInteractiveFiltersToggle = (target: "legend") => { const [state, dispatch] = useConfiguratorState(isConfiguring); + const chartConfig = getChartConfig(state); const onChange = useEvent((e: ChangeEvent) => { - const newConfig = produce( - state.chartConfig.interactiveFiltersConfig, - (draft) => { - if (draft?.[target]) { - draft[target].active = e.currentTarget.checked; - } - - return draft; - } - ); + if (chartConfig.interactiveFiltersConfig?.[target]) { + chartConfig.interactiveFiltersConfig[target].active = + e.currentTarget.checked; + } dispatch({ type: "INTERACTIVE_FILTER_CHANGED", - value: newConfig, + value: chartConfig.interactiveFiltersConfig, }); }); const stateValue = get( - state, - `chartConfig.interactiveFiltersConfig.${target}.active` + chartConfig, + `interactiveFiltersConfig.${target}.active` ); const checked = stateValue ? stateValue : false; @@ -49,34 +44,30 @@ export const useInteractiveTimeRangeFiltersToggle = ({ timeExtent: [string, string]; }) => { const [state, dispatch] = useConfiguratorState(isConfiguring); - const { chartConfig } = state; + const chartConfig = getChartConfig(state); const onChange = useCallback<(e: ChangeEvent) => void>( (e) => { const active = e.currentTarget.checked; if (timeExtent) { - const newConfig = produce( - chartConfig.interactiveFiltersConfig, - (draft) => { - if (draft?.timeRange) { - const { from, to } = draft.timeRange.presets; - draft.timeRange.active = active; - - // set min and max date as default presets for time brush - if (active && !from && !to) { - draft.timeRange.presets.from = timeExtent[0]; - draft.timeRange.presets.to = timeExtent[1]; - } - } - - return draft; + if (chartConfig.interactiveFiltersConfig?.timeRange) { + const { from, to } = + chartConfig.interactiveFiltersConfig.timeRange.presets; + chartConfig.interactiveFiltersConfig.timeRange.active = active; + + // set min and max date as default presets for time brush + if (active && !from && !to) { + chartConfig.interactiveFiltersConfig.timeRange.presets.from = + timeExtent[0]; + chartConfig.interactiveFiltersConfig.timeRange.presets.to = + timeExtent[1]; } - ); + } dispatch({ type: "INTERACTIVE_FILTER_CHANGED", - value: newConfig, + value: chartConfig.interactiveFiltersConfig, }); } }, @@ -84,8 +75,8 @@ export const useInteractiveTimeRangeFiltersToggle = ({ ); const stateValue = get( - state, - `chartConfig.interactiveFiltersConfig.timeRange.active` + chartConfig, + `interactiveFiltersConfig.timeRange.active` ); const checked = stateValue ? stateValue : false; @@ -121,7 +112,7 @@ export const useInteractiveDataFiltersToggle = ({ dimensions: DimensionMetadataFragment[]; }) => { const [state, dispatch] = useConfiguratorState(isConfiguring); - const { chartConfig } = state; + const chartConfig = getChartConfig(state); const onChange = useCallback<(e: ChangeEvent) => void>( (e) => { @@ -169,8 +160,9 @@ export const useInteractiveDataFiltersToggle = ({ */ export const useInteractiveDataFilterToggle = (dimensionIri: string) => { const [state, dispatch] = useConfiguratorState(isConfiguring); + const chartConfig = getChartConfig(state); const toggle = useEvent(() => { - const { interactiveFiltersConfig } = state.chartConfig; + const { interactiveFiltersConfig } = chartConfig; const newIFConfig = toggleInteractiveFilterDataDimension( interactiveFiltersConfig, dimensionIri @@ -182,7 +174,7 @@ export const useInteractiveDataFilterToggle = (dimensionIri: string) => { }); }); const checked = - state.chartConfig.interactiveFiltersConfig?.dataFilters.componentIris?.includes( + chartConfig.interactiveFiltersConfig?.dataFilters.componentIris?.includes( dimensionIri ); @@ -221,8 +213,9 @@ export const toggleInteractiveFilterDataDimension = produce( */ export const useInteractiveTimeRangeToggle = () => { const [state, dispatch] = useConfiguratorState(isConfiguring); + const chartConfig = getChartConfig(state); const toggle = useEvent(() => { - const { interactiveFiltersConfig } = state.chartConfig; + const { interactiveFiltersConfig } = chartConfig; const newIFConfig = toggleInteractiveTimeRangeFilter( interactiveFiltersConfig ); @@ -232,7 +225,7 @@ export const useInteractiveTimeRangeToggle = () => { value: newIFConfig, }); }); - const checked = state.chartConfig.interactiveFiltersConfig?.timeRange.active; + const checked = chartConfig.interactiveFiltersConfig?.timeRange.active; return { checked, toggle }; }; diff --git a/app/configurator/interactive-filters/interactive-filters-configurator.tsx b/app/configurator/interactive-filters/interactive-filters-configurator.tsx index 095f96f0c..1b49e3d35 100644 --- a/app/configurator/interactive-filters/interactive-filters-configurator.tsx +++ b/app/configurator/interactive-filters/interactive-filters-configurator.tsx @@ -4,6 +4,7 @@ import { getFieldComponentIri } from "@/charts"; import { ANIMATION_FIELD_SPEC } from "@/charts/chart-config-ui-options"; import { ConfiguratorStateConfiguringChart, + getChartConfig, isAnimationInConfig, } from "@/config-types"; import { @@ -26,10 +27,12 @@ export const isInteractiveFilterType = ( }; export const InteractiveFiltersConfigurator = ({ - state: { dataSet, dataSource, chartConfig }, + state, }: { state: ConfiguratorStateConfiguringChart; }) => { + const { dataSet, dataSource } = state; + const chartConfig = getChartConfig(state); const { fields } = chartConfig; const locale = useLocale(); const [{ data }] = useComponentsQuery({ diff --git a/app/configurator/table/table-chart-configurator.tsx b/app/configurator/table/table-chart-configurator.tsx index 3c8598c78..d51fc5e82 100644 --- a/app/configurator/table/table-chart-configurator.tsx +++ b/app/configurator/table/table-chart-configurator.tsx @@ -10,16 +10,23 @@ import { import { Loading } from "@/components/hint"; import { TableFields } from "@/config-types"; -import { ConfiguratorStateConfiguringChart } from "@/configurator"; +import { + ConfiguratorStateConfiguringChart, + getChartConfig, +} from "@/configurator"; import { TabDropZone } from "@/configurator/components/chart-controls/drag-and-drop-tab"; import { ControlSection, ControlSectionContent, SubsectionTitle, } from "@/configurator/components/chart-controls/section"; +import { ChartTypeSelector } from "@/configurator/components/chart-type-selector"; import { AnnotatorTabField } from "@/configurator/components/field"; import { useOrderedTableColumns } from "@/configurator/components/ui-helpers"; -import { useConfiguratorState } from "@/configurator/configurator-state"; +import { + isConfiguring, + useConfiguratorState, +} from "@/configurator/configurator-state"; import { moveFields } from "@/configurator/table/table-config-state"; import { useComponentsWithHierarchiesQuery, @@ -27,8 +34,6 @@ import { } from "@/graphql/query-hooks"; import { useLocale } from "@/locales/use-locale"; -import { ChartTypeSelector } from "../components/chart-type-selector"; - const useStyles = makeStyles((theme: Theme) => ({ emptyGroups: { textAlign: "center", @@ -84,7 +89,8 @@ export const ChartConfiguratorTable = ({ : null; }, [metadata?.dataCubeByIri, components?.dataCubeByIri]); - const [, dispatch] = useConfiguratorState(); + const [, dispatch] = useConfiguratorState(isConfiguring); + const chartConfig = getChartConfig(state); const [currentDraggableId, setCurrentDraggableId] = useState( null @@ -94,15 +100,11 @@ export const ChartConfiguratorTable = ({ ({ source, destination }) => { setCurrentDraggableId(null); - if ( - !destination || - state.chartConfig.chartType !== "table" || - !metaData - ) { + if (!destination || chartConfig.chartType !== "table" || !metaData) { return; } - const chartConfig = moveFields(state.chartConfig, { + const newChartConfig = moveFields(chartConfig, { source, destination, }); @@ -110,19 +112,19 @@ export const ChartConfiguratorTable = ({ dispatch({ type: "CHART_CONFIG_REPLACED", value: { - chartConfig, + chartConfig: newChartConfig, dataSetMetadata: metaData, }, }); }, - [state, dispatch, metaData] + [chartConfig, dispatch, metaData] ); const onDragStart = useCallback(({ draggableId }) => { setCurrentDraggableId(draggableId); }, []); - const fields = state.chartConfig.fields as TableFields; + const fields = chartConfig.fields as TableFields; const fieldsArray = useOrderedTableColumns(fields); if (metaData) { diff --git a/app/configurator/table/table-chart-options.tsx b/app/configurator/table/table-chart-options.tsx index fe9535ec5..0479bb480 100644 --- a/app/configurator/table/table-chart-options.tsx +++ b/app/configurator/table/table-chart-options.tsx @@ -8,6 +8,7 @@ import { Checkbox } from "@/components/form"; import { ColumnStyle, ConfiguratorStateConfiguringChart, + getChartConfig, isTableConfig, TableConfig, } from "@/config-types"; @@ -31,7 +32,10 @@ import { } from "@/configurator/components/filters"; import { mapValueIrisToColor } from "@/configurator/components/ui-helpers"; import { FieldProps } from "@/configurator/config-form"; -import { useConfiguratorState } from "@/configurator/configurator-state"; +import { + isConfiguring, + useConfiguratorState, +} from "@/configurator/configurator-state"; import { TableSortingOptions } from "@/configurator/table/table-chart-sorting-options"; import { updateIsGroup, @@ -63,16 +67,13 @@ const useTableColumnGroupHiddenField = ({ field: string; metaData: DataCubeMetadata; }): FieldProps => { - const [state, dispatch] = useConfiguratorState(); - + const [state, dispatch] = useConfiguratorState(isConfiguring); + const chartConfig = getChartConfig(state); const onChange = useCallback<(e: ChangeEvent) => void>( (e) => { - if ( - state.state === "CONFIGURING_CHART" && - isTableConfig(state.chartConfig) - ) { + if (isTableConfig(chartConfig)) { const updater = path === "isGroup" ? updateIsGroup : updateIsHidden; - const chartConfig = updater(state.chartConfig, { + const newChartConfig = updater(chartConfig, { field, value: e.currentTarget.checked, }); @@ -80,18 +81,15 @@ const useTableColumnGroupHiddenField = ({ dispatch({ type: "CHART_CONFIG_REPLACED", value: { - chartConfig, + chartConfig: newChartConfig, dataSetMetadata: metaData, }, }); } }, - [state, path, field, dispatch, metaData] + [path, chartConfig, field, dispatch, metaData] ); - const stateValue = - state.state === "CONFIGURING_CHART" - ? get(state, `chartConfig.fields["${field}"].${path}`, "") - : ""; + const stateValue = get(chartConfig, `fields["${field}"].${path}`, ""); const checked = stateValue ? stateValue : false; return { @@ -139,7 +137,8 @@ export const TableColumnOptions = ({ state: ConfiguratorStateConfiguringChart; metaData: DataCubeMetadataWithHierarchies; }) => { - const { activeField: _activeField, chartConfig } = state; + const chartConfig = getChartConfig(state); + const { activeField: _activeField } = chartConfig; const activeField = _activeField as EncodingFieldType | undefined; if (!activeField || chartConfig.chartType !== "table") { diff --git a/app/configurator/table/table-chart-sorting-options.tsx b/app/configurator/table/table-chart-sorting-options.tsx index d5ddaa15b..d6bdfddbd 100644 --- a/app/configurator/table/table-chart-sorting-options.tsx +++ b/app/configurator/table/table-chart-sorting-options.tsx @@ -21,6 +21,7 @@ import { Radio, Select } from "@/components/form"; import VisuallyHidden from "@/components/visually-hidden"; import { ConfiguratorStateConfiguringChart, + getChartConfig, TableConfig, TableSortingOption, } from "@/config-types"; @@ -346,14 +347,15 @@ export const TableSortingOptions = ({ dataSetMetadata: DataCubeMetadataWithHierarchies; }) => { const [, dispatch] = useConfiguratorState(); - const { activeField, chartConfig } = state; + const chartConfig = getChartConfig(state); + const { activeField } = chartConfig; const classes = useStyles(); const onDragEnd = useCallback( ({ source, destination }) => { if ( !destination || - state.chartConfig.chartType !== "table" || + chartConfig.chartType !== "table" || !dataSetMetadata ) { return; @@ -362,7 +364,7 @@ export const TableSortingOptions = ({ dispatch({ type: "CHART_CONFIG_REPLACED", value: { - chartConfig: moveSortingOptions(state.chartConfig, { + chartConfig: moveSortingOptions(chartConfig, { source, destination, }), @@ -370,7 +372,7 @@ export const TableSortingOptions = ({ }, }); }, - [state, dispatch, dataSetMetadata] + [chartConfig, dataSetMetadata, dispatch] ); if (!activeField || chartConfig.chartType !== "table") { diff --git a/app/db/config.ts b/app/db/config.ts index d0f7ddced..eff7467d2 100644 --- a/app/db/config.ts +++ b/app/db/config.ts @@ -4,15 +4,13 @@ import { Config, Prisma, User } from "@prisma/client"; -import { ChartConfig, Config as ConfigType } from "@/configurator"; -import { migrateChartConfig } from "@/utils/chart-config/versioning"; +import { ChartConfig } from "@/configurator"; +import { migrateConfiguratorState } from "@/utils/chart-config/versioning"; import { createChartId } from "../utils/create-chart-id"; import prisma from "./client"; -type PublishedConfig = Omit; - /** * Store data in the DB. * If the user is logged, the chart is linked to the user. @@ -64,16 +62,17 @@ type ChartJsonConfig = { chartConfig: Prisma.JsonObject; }; -const parseDbConfig = (conf: Config) => { - const data = conf.data as ChartJsonConfig; - const migratedData = { - ...data, - dataSet: migrateDataSet(data.dataSet), - chartConfig: ensureFiltersOrder(migrateChartConfig(data.chartConfig)), - } as PublishedConfig; +const parseDbConfig = (d: Config) => { + const data = d.data as ChartJsonConfig; + const migratedData = migrateConfiguratorState(data); + return { - ...conf, - data: migratedData, + ...d, + data: { + ...migratedData, + dataSet: migrateDataSet(migratedData.dataSet), + chartConfigs: migratedData.chartConfigs.map(ensureFiltersOrder), + }, }; }; diff --git a/app/docs/annotations.docs.tsx b/app/docs/annotations.docs.tsx index ae4c5525c..794eec48c 100644 --- a/app/docs/annotations.docs.tsx +++ b/app/docs/annotations.docs.tsx @@ -19,6 +19,7 @@ import { measures, observations, } from "@/docs/fixtures"; +import { CHART_CONFIG_VERSION } from "@/utils/chart-config/versioning"; export default () => markdown` @@ -38,7 +39,22 @@ ${( dimensions={dimensions} dimensionsByIri={keyBy(dimensions, (d) => d.iri)} chartConfig={{ - version: "1.4.2", + key: "column-chart", + version: CHART_CONFIG_VERSION, + meta: { + title: { + en: "", + de: "", + fr: "", + it: "", + }, + description: { + en: "", + de: "", + fr: "", + it: "", + }, + }, chartType: "column", fields, interactiveFiltersConfig: { @@ -52,6 +68,7 @@ ${( calculation: { active: false, type: "identity" }, }, filters: {}, + activeField: undefined, }} aspectRatio={0.4} > @@ -184,7 +201,22 @@ ${( dimensions={dimensions} dimensionsByIri={keyBy(dimensions, (d) => d.iri)} chartConfig={{ - version: "1.4.2", + key: "column-chart", + version: CHART_CONFIG_VERSION, + meta: { + title: { + en: "", + de: "", + fr: "", + it: "", + }, + description: { + en: "", + de: "", + fr: "", + it: "", + }, + }, chartType: "column", fields, interactiveFiltersConfig: { @@ -198,6 +230,7 @@ ${( calculation: { active: false, type: "identity" }, }, filters: {}, + activeField: undefined, }} aspectRatio={0.4} > @@ -229,7 +262,22 @@ ${( dimensions={dimensions} dimensionsByIri={keyBy(dimensions, (d) => d.iri)} chartConfig={{ - version: "1.4.2", + key: "column-chart", + version: CHART_CONFIG_VERSION, + meta: { + title: { + en: "", + de: "", + fr: "", + it: "", + }, + description: { + en: "", + de: "", + fr: "", + it: "", + }, + }, chartType: "column", fields, interactiveFiltersConfig: { @@ -243,6 +291,7 @@ ${( calculation: { active: false, type: "identity" }, }, filters: {}, + activeField: undefined, }} aspectRatio={0.4} > diff --git a/app/docs/columns.docs.tsx b/app/docs/columns.docs.tsx index 57b0833a8..3a50815ec 100644 --- a/app/docs/columns.docs.tsx +++ b/app/docs/columns.docs.tsx @@ -11,6 +11,7 @@ import { import { ChartContainer, ChartSvg } from "@/charts/shared/containers"; import { Tooltip } from "@/charts/shared/interaction/tooltip"; import { DimensionMetadataFragment } from "@/graphql/query-hooks"; +import { CHART_CONFIG_VERSION } from "@/utils/chart-config/versioning"; export const Docs = () => markdown` @@ -25,8 +26,23 @@ ${( dimensions={columnDimensions} dimensionsByIri={keyBy(columnDimensions, (d) => d.iri)} chartConfig={{ + key: "column-chart", chartType: "column", - version: "1.4.2", + version: CHART_CONFIG_VERSION, + meta: { + title: { + en: "", + de: "", + fr: "", + it: "", + }, + description: { + en: "", + de: "", + fr: "", + it: "", + }, + }, filters: {}, fields: columnFields, interactiveFiltersConfig: { @@ -52,6 +68,7 @@ ${( type: "identity", }, }, + activeField: undefined, }} aspectRatio={0.4} > diff --git a/app/docs/fixtures.ts b/app/docs/fixtures.ts index 44d85916b..8974155bc 100644 --- a/app/docs/fixtures.ts +++ b/app/docs/fixtures.ts @@ -8,7 +8,7 @@ export const states: ConfiguratorState[] = [ state: "SELECTING_DATASET", dataSet: undefined, dataSource: DEFAULT_DATA_SOURCE, - chartConfig: undefined, + chartConfigs: undefined, meta: { title: { de: "", @@ -23,49 +23,67 @@ export const states: ConfiguratorState[] = [ en: "", }, }, - activeField: undefined, + activeChartKey: undefined, }, { state: "CONFIGURING_CHART", dataSet: "foo", dataSource: DEFAULT_DATA_SOURCE, - chartConfig: { - version: "1.2.1", - chartType: "column", - fields: { - x: { - componentIri: "foo", - sorting: { sortingType: "byDimensionLabel", sortingOrder: "asc" }, - }, - y: { - componentIri: "foo", - }, - }, - filters: {}, - interactiveFiltersConfig: { - legend: { - active: false, - componentIri: "", - }, - timeRange: { - active: false, - componentIri: "", - presets: { - type: "range", - from: "", - to: "", + chartConfigs: [ + { + key: "column", + version: "1.2.1", + meta: { + title: { + en: "", + de: "", + fr: "", + it: "", + }, + description: { + en: "", + de: "", + fr: "", + it: "", }, }, - dataFilters: { - active: false, - componentIris: [], + chartType: "column", + fields: { + x: { + componentIri: "foo", + sorting: { sortingType: "byDimensionLabel", sortingOrder: "asc" }, + }, + y: { + componentIri: "foo", + }, }, - calculation: { - active: false, - type: "identity", + filters: {}, + interactiveFiltersConfig: { + legend: { + active: false, + componentIri: "", + }, + timeRange: { + active: false, + componentIri: "", + presets: { + type: "range", + from: "", + to: "", + }, + }, + dataFilters: { + active: false, + componentIris: [], + }, + calculation: { + active: false, + type: "identity", + }, }, + activeField: undefined, }, - }, + ], meta: { title: { de: "", @@ -80,7 +98,7 @@ export const states: ConfiguratorState[] = [ en: "", }, }, - activeField: undefined, + activeChartKey: undefined, }, ]; @@ -790,7 +808,22 @@ export const tableDimensions = [ }, ]; export const tableConfig: TableConfig = { + key: "table", version: "1.2.1", + meta: { + title: { + en: "", + de: "", + fr: "", + it: "", + }, + description: { + en: "", + de: "", + fr: "", + it: "", + }, + }, chartType: "table", filters: {}, interactiveFiltersConfig: undefined, @@ -1015,4 +1048,5 @@ export const tableConfig: TableConfig = { }, }, }, + activeField: undefined, }; diff --git a/app/docs/lines.docs.tsx b/app/docs/lines.docs.tsx index 6f399c690..2ab358d8e 100644 --- a/app/docs/lines.docs.tsx +++ b/app/docs/lines.docs.tsx @@ -15,6 +15,7 @@ import { } from "@/configurator"; import { PublishedConfiguratorStateProvider } from "@/configurator/configurator-state"; import { DimensionMetadataFragment } from "@/graphql/query-hooks"; +import { CHART_CONFIG_VERSION } from "@/utils/chart-config/versioning"; export const Docs = () => markdown` @@ -118,11 +119,27 @@ const fields = { }; const chartConfig: LineConfig = { + key: "line", + version: CHART_CONFIG_VERSION, + meta: { + title: { + en: "", + de: "", + fr: "", + it: "", + }, + description: { + en: "", + de: "", + fr: "", + it: "", + }, + }, chartType: "line", - filters: {}, - version: "1.4.2", interactiveFiltersConfig, fields, + filters: {}, + activeField: undefined, }; const measures = [ diff --git a/app/docs/scatterplot.docs.tsx b/app/docs/scatterplot.docs.tsx index 0da9dfc0c..757cf8be2 100644 --- a/app/docs/scatterplot.docs.tsx +++ b/app/docs/scatterplot.docs.tsx @@ -18,6 +18,7 @@ import { InteractionVoronoi } from "@/charts/shared/overlay-voronoi"; import { InteractiveFiltersConfig, ScatterPlotConfig } from "@/config-types"; import { PublishedConfiguratorStateProvider } from "@/configurator/configurator-state"; import { DimensionMetadataFragment } from "@/graphql/query-hooks"; +import { CHART_CONFIG_VERSION } from "@/utils/chart-config/versioning"; export const Docs = () => markdown` @@ -117,11 +118,27 @@ const scatterplotFields = { }; const chartConfig: ScatterPlotConfig = { + key: "scatterplot", + version: CHART_CONFIG_VERSION, + meta: { + title: { + en: "", + de: "", + fr: "", + it: "", + }, + description: { + en: "", + de: "", + fr: "", + it: "", + }, + }, chartType: "scatterplot", filters: {}, - version: "1.4.2", interactiveFiltersConfig, fields: scatterplotFields, + activeField: undefined, }; const scatterplotMeasures = [ diff --git a/app/homepage/examples.tsx b/app/homepage/examples.tsx index 8435f8213..8cc503e4f 100644 --- a/app/homepage/examples.tsx +++ b/app/homepage/examples.tsx @@ -52,80 +52,100 @@ export const Examples = ({ it: "", }, }} - chartConfig={migrateChartConfig({ - version: "1.2.1", - fields: { - x: { - sorting: { - sortingType: "byMeasure", - sortingOrder: "desc", + chartConfig={migrateChartConfig( + { + version: "1.2.1", + fields: { + x: { + sorting: { + sortingType: "byMeasure", + sortingOrder: "desc", + }, + componentIri: + "https://environment.ld.admin.ch/foen/ubd003701/verkehrsart", }, - componentIri: - "https://environment.ld.admin.ch/foen/ubd003701/verkehrsart", - }, - y: { - componentIri: - "https://environment.ld.admin.ch/foen/ubd003701/wert", - }, - segment: { - type: "grouped", - palette: "category10", - sorting: { - sortingType: "byTotalSize", - sortingOrder: "asc", + y: { + componentIri: + "https://environment.ld.admin.ch/foen/ubd003701/wert", }, - colorMapping: { - "https://environment.ld.admin.ch/foen/ubd003701/periode/D": - "#ff7f0e", - "https://environment.ld.admin.ch/foen/ubd003701/periode/N": - "#1f77b4", + segment: { + type: "grouped", + palette: "category10", + sorting: { + sortingType: "byTotalSize", + sortingOrder: "asc", + }, + colorMapping: { + "https://environment.ld.admin.ch/foen/ubd003701/periode/D": + "#ff7f0e", + "https://environment.ld.admin.ch/foen/ubd003701/periode/N": + "#1f77b4", + }, + componentIri: + "https://environment.ld.admin.ch/foen/ubd003701/periode", }, - componentIri: - "https://environment.ld.admin.ch/foen/ubd003701/periode", - }, - }, - filters: { - "https://environment.ld.admin.ch/foen/ubd003701/beurteilung": { - type: "single", - value: - "https://environment.ld.admin.ch/foen/ubd003701/beurteilung/%3EIGWLSV", }, - "https://environment.ld.admin.ch/foen/ubd003701/gemeindetype": { - type: "single", - value: - "https://environment.ld.admin.ch/foen/ubd003701/gemeindeTyp/CH", - }, - "https://environment.ld.admin.ch/foen/ubd003701/laermbelasteteeinheit": - { + filters: { + "https://environment.ld.admin.ch/foen/ubd003701/beurteilung": { type: "single", value: - "https://environment.ld.admin.ch/foen/ubd003701/laermbelasteteEinheit/Pers", + "https://environment.ld.admin.ch/foen/ubd003701/beurteilung/%3EIGWLSV", }, - }, - chartType: "column", - interactiveFiltersConfig: { - timeRange: { - active: false, - presets: { - to: "", - from: "", - type: "range", + "https://environment.ld.admin.ch/foen/ubd003701/gemeindetype": { + type: "single", + value: + "https://environment.ld.admin.ch/foen/ubd003701/gemeindeTyp/CH", }, - componentIri: "", - }, - legend: { - active: false, - componentIri: "", + "https://environment.ld.admin.ch/foen/ubd003701/laermbelasteteeinheit": + { + type: "single", + value: + "https://environment.ld.admin.ch/foen/ubd003701/laermbelasteteEinheit/Pers", + }, }, - dataFilters: { - active: true, - componentIris: [ - "https://environment.ld.admin.ch/foen/ubd003701/gemeindetype", - "https://environment.ld.admin.ch/foen/ubd003701/laermbelasteteeinheit", - ], + chartType: "column", + interactiveFiltersConfig: { + timeRange: { + active: false, + presets: { + to: "", + from: "", + type: "range", + }, + componentIri: "", + }, + legend: { + active: false, + componentIri: "", + }, + dataFilters: { + active: true, + componentIris: [ + "https://environment.ld.admin.ch/foen/ubd003701/gemeindetype", + "https://environment.ld.admin.ch/foen/ubd003701/laermbelasteteeinheit", + ], + }, }, }, - })} + { + migrationProps: { + meta: { + title: { + de: "Lärmbelastung durch Verkehr", + en: "Traffic noise pollution", + fr: "Exposition au bruit du trafic", + it: "Esposizione al rumore del traffico", + }, + description: { + de: "", + en: "", + fr: "", + it: "", + }, + }, + }, + } + )} configKey={""} /> @@ -151,61 +171,81 @@ export const Examples = ({ it: "", }, }} - chartConfig={migrateChartConfig({ - version: "1.2.1", - fields: { - x: { - componentIri: "http://www.w3.org/2006/time#Year", + chartConfig={migrateChartConfig( + { + version: "1.2.1", + fields: { + x: { + componentIri: "http://www.w3.org/2006/time#Year", + }, + y: { + componentIri: "http://schema.org/amount", + }, + segment: { + palette: "category10", + sorting: { + sortingType: "byDimensionLabel", + sortingOrder: "asc", + }, + colorMapping: { + "https://culture.ld.admin.ch/sfa/StateAccounts_Office/OperationCharacter/OC1": + "#1f77b4", + "https://culture.ld.admin.ch/sfa/StateAccounts_Office/OperationCharacter/OC2": + "#ff7f0e", + }, + componentIri: + "https://culture.ld.admin.ch/sfa/StateAccounts_Office/operationcharacter", + }, }, - y: { - componentIri: "http://schema.org/amount", + filters: { + "https://culture.ld.admin.ch/sfa/StateAccounts_Office/office": { + type: "single", + value: + "https://culture.ld.admin.ch/sfa/StateAccounts_Office/Office/O7", + }, }, - segment: { - palette: "category10", - sorting: { - sortingType: "byDimensionLabel", - sortingOrder: "asc", + chartType: "area", + interactiveFiltersConfig: { + timeRange: { + active: true, + presets: { + to: "2013-12-31T23:00:00.000Z", + from: "1950-12-31T23:00:00.000Z", + type: "range", + }, + componentIri: "", }, - colorMapping: { - "https://culture.ld.admin.ch/sfa/StateAccounts_Office/OperationCharacter/OC1": - "#1f77b4", - "https://culture.ld.admin.ch/sfa/StateAccounts_Office/OperationCharacter/OC2": - "#ff7f0e", + legend: { + active: true, + componentIri: "", + }, + dataFilters: { + active: true, + componentIris: [ + "https://culture.ld.admin.ch/sfa/StateAccounts_Office/office", + ], }, - componentIri: - "https://culture.ld.admin.ch/sfa/StateAccounts_Office/operationcharacter", - }, - }, - filters: { - "https://culture.ld.admin.ch/sfa/StateAccounts_Office/office": { - type: "single", - value: - "https://culture.ld.admin.ch/sfa/StateAccounts_Office/Office/O7", }, }, - chartType: "area", - interactiveFiltersConfig: { - timeRange: { - active: true, - presets: { - to: "2013-12-31T23:00:00.000Z", - from: "1950-12-31T23:00:00.000Z", - type: "range", + { + migrationProps: { + meta: { + title: { + de: "Lärmbelastung durch Verkehr", + en: "Traffic noise pollution", + fr: "Exposition au bruit du trafic", + it: "Esposizione al rumore del traffico", + }, + description: { + de: "", + en: "", + fr: "", + it: "", + }, }, - componentIri: "", }, - legend: { - active: true, - componentIri: "", - }, - dataFilters: { - active: true, - componentIris: [ - "https://culture.ld.admin.ch/sfa/StateAccounts_Office/office", - ], - }, - }, - })} + } + )} configKey={""} /> diff --git a/app/pages/__test/[env]/[slug].tsx b/app/pages/__test/[env]/[slug].tsx index ea0c73c5d..773968e11 100644 --- a/app/pages/__test/[env]/[slug].tsx +++ b/app/pages/__test/[env]/[slug].tsx @@ -31,7 +31,9 @@ const Page: NextPage = () => { if (config) { const { dataSet, dataSource, meta, chartConfig } = config.data; - const migratedConfig = migrateChartConfig(chartConfig); + const migratedConfig = migrateChartConfig(chartConfig, { + migrationProps: config.data, + }); return ( diff --git a/app/pages/_charts.tsx b/app/pages/_charts.tsx index 2c9911a56..b8f409a1e 100644 --- a/app/pages/_charts.tsx +++ b/app/pages/_charts.tsx @@ -89,16 +89,16 @@ const Page: NextPage = ({ configs }) => { {configs.map( ( - { key, data: { dataSet, dataSource, chartConfig, meta } }, + { key, data: { dataSet, dataSource, chartConfigs, meta } }, i ) => { - return ( + return chartConfigs.map((d) => ( - + Loading...} @@ -106,7 +106,7 @@ const Page: NextPage = ({ configs }) => { @@ -125,7 +125,7 @@ const Page: NextPage = ({ configs }) => { - ); + )); } )} diff --git a/app/pages/embed/[chartId].tsx b/app/pages/embed/[chartId].tsx index 94f122d2d..4e0038275 100644 --- a/app/pages/embed/[chartId].tsx +++ b/app/pages/embed/[chartId].tsx @@ -3,7 +3,7 @@ import { GetServerSideProps } from "next"; import ErrorPage from "next/error"; import { ChartPublished } from "@/components/chart-published"; -import { Config } from "@/configurator"; +import { Config, getChartConfig } from "@/configurator"; import { getConfig } from "@/db/config"; import { serializeProps } from "@/db/serialize"; import { EmbedOptionsProvider } from "@/utils/embed"; @@ -43,19 +43,17 @@ const EmbedPage = (props: PageProps) => { } const { - config: { - key, - data: { dataSet, dataSource, meta, chartConfig }, - }, + config: { key, data }, } = props; + const chartConfig = getChartConfig({ ...data, state: "PUBLISHING" }); return ( diff --git a/app/pages/v/[chartId].tsx b/app/pages/v/[chartId].tsx index b724d7339..989818b94 100644 --- a/app/pages/v/[chartId].tsx +++ b/app/pages/v/[chartId].tsx @@ -13,9 +13,9 @@ import { ChartPublished } from "@/components/chart-published"; import { Success } from "@/components/hint"; import { ContentLayout } from "@/components/layout"; import { PublishActions } from "@/components/publish-actions"; -import { Config } from "@/configurator"; +import { Config, getChartConfig } from "@/configurator"; import { getConfig } from "@/db/config"; -import { deserializeProps, Serialized, serializeProps } from "@/db/serialize"; +import { Serialized, deserializeProps, serializeProps } from "@/db/serialize"; import { useLocale } from "@/locales/use-locale"; import { useDataSourceStore } from "@/stores/data-source"; import { EmbedOptionsProvider } from "@/utils/embed"; @@ -28,7 +28,7 @@ type PageProps = status: "found"; config: { key: string; - data: Omit; + data: Config; }; }; @@ -96,6 +96,7 @@ const VisualizationPage = (props: Serialized) => { const { key, data } = (props as Exclude) .config; + const chartConfig = getChartConfig({ ...data, state: "PUBLISHING" }); return ( @@ -127,11 +128,11 @@ const VisualizationPage = (props: Serialized) => { )} - + diff --git a/app/utils/chart-config/api.ts b/app/utils/chart-config/api.ts index 14d575382..12eb9fc92 100644 --- a/app/utils/chart-config/api.ts +++ b/app/utils/chart-config/api.ts @@ -14,14 +14,14 @@ export const createConfig = async (state: ConfiguratorStatePublishing) => { dataSet: state.dataSet, dataSource: state.dataSource, meta: state.meta, - chartConfig: state.chartConfig, + chartConfigs: state.chartConfigs, }, }, }); }; -export const fetchChartConfig = async (chartId: string) => { +export const fetchChartConfig = async (id: string) => { return await apiFetch>( - `/api/config/${chartId}` + `/api/config/${id}` ); }; diff --git a/app/utils/chart-config/versioning.spec.ts b/app/utils/chart-config/versioning.spec.ts index e1430ab9a..f26cebba2 100644 --- a/app/utils/chart-config/versioning.spec.ts +++ b/app/utils/chart-config/versioning.spec.ts @@ -1,7 +1,29 @@ -import { decodeChartConfig, LineConfig, MapConfig } from "@/config-types"; +import { + ConfiguratorStateConfiguringChart, + decodeChartConfig, + LineConfig, + MapConfig, +} from "@/config-types"; import { migrateChartConfig } from "./versioning"; +const CONFIGURATOR_STATE = { + meta: { + title: { + de: "", + fr: "", + it: "", + en: "", + }, + description: { + de: "", + fr: "", + it: "", + en: "", + }, + }, +} as unknown as ConfiguratorStateConfiguringChart; + describe("config migrations", () => { const oldMapConfig = { version: "1.0.0", @@ -81,7 +103,9 @@ describe("config migrations", () => { }; it("should migrate to newest config and back (but might lost some info for major version changes", () => { - const migratedConfig = migrateChartConfig(oldMapConfig); + const migratedConfig = migrateChartConfig(oldMapConfig, { + migrationProps: CONFIGURATOR_STATE, + }); const decodedConfig = decodeChartConfig(migratedConfig); expect(decodedConfig).toBeDefined(); @@ -115,7 +139,9 @@ describe("config migrations", () => { }); it("should correctly migrate interactiveFiltersConfig", () => { - const migratedConfig = migrateChartConfig(oldLineConfig); + const migratedConfig = migrateChartConfig(oldLineConfig, { + migrationProps: CONFIGURATOR_STATE, + }); const decodedConfig = decodeChartConfig(migratedConfig); expect(decodedConfig).toBeDefined(); diff --git a/app/utils/chart-config/versioning.ts b/app/utils/chart-config/versioning.ts index 20977bbec..2bbf1d150 100644 --- a/app/utils/chart-config/versioning.ts +++ b/app/utils/chart-config/versioning.ts @@ -1,16 +1,18 @@ import produce from "immer"; -export const CHART_CONFIG_VERSION = "1.4.2"; +import { createChartId } from "@/utils/create-chart-id"; type Migration = { description: string; from: string; to: string; - up: (config: any) => any; - down: (config: any) => any; + up: (config: any, migrationProps?: any) => any; + down: (config: any, migrationProps?: any) => any; }; -const migrations: Migration[] = [ +export const CHART_CONFIG_VERSION = "2.0.0"; + +const chartConfigMigrations: Migration[] = [ { description: `MAP baseLayer { @@ -19,7 +21,7 @@ const migrations: Migration[] = [ }`, from: "1.0.0", to: "1.0.1", - up: (config: any) => { + up: (config) => { let newConfig = { ...config, version: "1.0.1" }; if (newConfig.chartType === "map") { @@ -34,7 +36,7 @@ const migrations: Migration[] = [ return newConfig; }, - down: (config: any) => { + down: (config) => { let newConfig = { ...config, version: "1.0.0" }; if (newConfig.chartType === "map") { @@ -61,7 +63,7 @@ const migrations: Migration[] = [ }`, from: "1.0.1", to: "1.0.2", - up: (config: any) => { + up: (config) => { let newConfig = { ...config, version: "1.0.2" }; if (newConfig.chartType === "map") { @@ -84,7 +86,7 @@ const migrations: Migration[] = [ return newConfig; }, - down: (config: any) => { + down: (config) => { let newConfig = { ...config, version: "1.0.1" }; if (newConfig.chartType === "map") { @@ -115,7 +117,7 @@ const migrations: Migration[] = [ }`, from: "1.0.2", to: "1.1.0", - up: (config: any) => { + up: (config) => { let newConfig = { ...config, version: "1.1.0" }; if (newConfig.chartType === "map") { @@ -144,7 +146,7 @@ const migrations: Migration[] = [ return newConfig; }, - down: (config: any) => { + down: (config) => { let newConfig = { ...config, version: "1.0.2" }; if (newConfig.chartType === "map") { @@ -256,7 +258,7 @@ const migrations: Migration[] = [ }`, from: "1.1.0", to: "1.1.1", - up: (config: any) => { + up: (config) => { let newConfig = { ...config, version: "1.1.1" }; if (newConfig.chartType === "map") { @@ -319,7 +321,7 @@ const migrations: Migration[] = [ return newConfig; }, - down: (config: any) => { + down: (config) => { let newConfig = { ...config, version: "1.1.0" }; if (newConfig.chartType === "map") { @@ -336,7 +338,7 @@ const migrations: Migration[] = [ palette: color.palette, colorScaleType: color.scaleType, colorInterpolationType: color.interpolationType, - nbClass: color.nbClass || 3, + nbClass: color.nbClass ?? 3, }; }); } @@ -369,12 +371,12 @@ const migrations: Migration[] = [ `, from: "1.1.1", to: "1.2.0", - up: (config: any) => { + up: (config) => { let newConfig = { ...config, version: "1.2.0" }; return newConfig; }, - down: (config: any) => { + down: (config) => { let newConfig = { ...config, version: "1.1.1" }; if (newConfig.chartType === "map") { @@ -398,7 +400,7 @@ const migrations: Migration[] = [ }`, from: "1.2.0", to: "1.2.1", - up: (config: any) => { + up: (config) => { let newConfig = { ...config, version: "1.2.1" }; const { interactiveFiltersConfig } = newConfig; @@ -415,7 +417,7 @@ const migrations: Migration[] = [ return newConfig; }, - down: (config: any) => { + down: (config) => { let newConfig = { ...config, version: "1.2.0" }; const { interactiveFiltersConfig } = newConfig; @@ -440,7 +442,7 @@ const migrations: Migration[] = [ }`, from: "1.2.1", to: "1.3.0", - up: (config: any) => { + up: (config) => { let newConfig = { ...config, version: "1.3.0" }; const { interactiveFiltersConfig } = newConfig; @@ -458,7 +460,7 @@ const migrations: Migration[] = [ return newConfig; }, - down: (config: any) => { + down: (config) => { let newConfig = { ...config, version: "1.2.1" }; const { interactiveFiltersConfig } = newConfig; @@ -485,7 +487,7 @@ const migrations: Migration[] = [ }`, from: "1.3.0", to: "1.4.0", - up: (config: any) => { + up: (config) => { let newConfig = { ...config, version: "1.4.0" }; const { interactiveFiltersConfig } = newConfig; @@ -514,7 +516,7 @@ const migrations: Migration[] = [ return newConfig; }, - down: (config: any) => { + down: (config) => { let newConfig = { ...config, version: "1.3.0" }; const { fields } = config; @@ -546,7 +548,7 @@ const migrations: Migration[] = [ }`, from: "1.4.0", to: "1.4.1", - up: (config: any) => { + up: (config) => { let newConfig = { ...config, version: "1.4.1" }; const { fields, interactiveFiltersConfig } = newConfig; @@ -570,7 +572,7 @@ const migrations: Migration[] = [ return newConfig; }, - down: (config: any) => { + down: (config) => { let newConfig = { ...config, version: "1.4.0" }; const { interactiveFiltersConfig } = config; @@ -594,7 +596,7 @@ const migrations: Migration[] = [ }`, from: "1.4.1", to: "1.4.2", - up: (config: any) => { + up: (config) => { let newConfig = { ...config, version: "1.4.2" }; const { interactiveFiltersConfig } = newConfig; @@ -613,7 +615,7 @@ const migrations: Migration[] = [ return newConfig; }, - down: (config: any) => { + down: (config) => { let newConfig = { ...config, version: "1.4.1" }; const { interactiveFiltersConfig } = config; @@ -628,42 +630,126 @@ const migrations: Migration[] = [ return newConfig; }, }, + { + description: `ALL + key`, + from: "1.4.2", + to: "2.0.0", + up: (config, configuratorState) => { + const newConfig = { ...config, version: "2.0.0" }; + + return produce(newConfig, (draft: any) => { + draft.key = createChartId(); + draft.meta = configuratorState.meta; + draft.activeField = configuratorState.activeField; + }); + }, + down: (config) => { + const newConfig = { ...config, version: "1.4.2" }; + + return produce(newConfig, (draft: any) => { + delete draft.key; + }); + }, + }, +]; + +export const migrateChartConfig = makeMigrate(chartConfigMigrations, { + defaultToVersion: CHART_CONFIG_VERSION, +}); + +export const CONFIGURATOR_STATE_VERSION = "2.0.0"; + +const configuratorStateMigrations: Migration[] = [ + { + description: `ALL`, + from: "1.0.0", + to: "2.0.0", + up: (config: any) => { + const newConfig = { ...config, version: "2.0.0" }; + + return produce(newConfig, (draft: any) => { + const migratedChartConfig = migrateChartConfig(draft.chartConfig, { + migrationProps: draft, + toVersion: "2.0.0", + }); + draft.chartConfigs = [migratedChartConfig]; + delete draft.chartConfig; + delete draft.activeField; + draft.activeChartKey = migratedChartConfig.key; + }); + }, + down: (config: any) => { + const newConfig = { ...config, version: "1.0.0" }; + + return produce(newConfig, (draft: any) => { + const chartConfig = draft.chartConfigs[0]; + delete draft.chartConfigs; + delete draft.activeChartKey; + draft.meta = chartConfig.meta; + draft.activeField = chartConfig.activeField; + const migratedChartConfig = migrateChartConfig(chartConfig, { + toVersion: "1.4.2", + }); + draft.chartConfig = migratedChartConfig; + }); + }, + }, ]; -export const migrateChartConfig = ( - config: any, +export const migrateConfiguratorState = makeMigrate( + configuratorStateMigrations, { - fromVersion, - toVersion = CHART_CONFIG_VERSION, - }: { fromVersion?: string; toVersion?: string } = {} -) => { - const _migrateChartConfig = ( - config: any, - { + defaultToVersion: CONFIGURATOR_STATE_VERSION, + } +); + +function makeMigrate( + migrations: Migration[], + options: { defaultToVersion: string } +) { + const { defaultToVersion } = options; + + return ( + data: any, + options: { + fromVersion?: string; + toVersion?: string; + migrationProps?: any; + } = {} + ) => { + const { fromVersion, - toVersion = CHART_CONFIG_VERSION, - }: { fromVersion?: string; toVersion?: string } = {} - ): any => { - const fromVersionFinal = fromVersion || config.version || "1.0.0"; - const direction = upOrDown(fromVersionFinal, toVersion); - - if (direction === "same") { - return config; - } + toVersion = defaultToVersion, + migrationProps, + } = options; + const migrate = ( + data: any, + { + fromVersion, + }: { + fromVersion?: string; + } = {} + ): any => { + const fromVersionFinal = fromVersion ?? data.version ?? "1.0.0"; + const direction = upOrDown(fromVersionFinal, toVersion); + + if (direction === "same") { + return data; + } - const currentMigration = migrations.find( - (migration) => - migration[direction === "up" ? "from" : "to"] === fromVersionFinal - ); + const migration = migrations.find( + (d) => d[direction === "up" ? "from" : "to"] === fromVersionFinal + ); - if (currentMigration) { - const newConfig = currentMigration[direction](config); - return _migrateChartConfig(newConfig, { fromVersion, toVersion }); - } - }; + if (migration) { + const newData = migration[direction](data, migrationProps); + return migrate(newData, { fromVersion }); + } + }; - return _migrateChartConfig(config, { fromVersion, toVersion }); -}; + return migrate(data, { fromVersion }); + }; +} const upOrDown = ( fromVersion: string, From 824587ba79395675d3cc8e7347e4df6484b3c67a Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Tue, 5 Sep 2023 12:15:20 +0200 Subject: [PATCH 04/40] fix: Keep chart key when getting adjusted chart config --- app/charts/index.ts | 5 ++++- app/configurator/configurator-state.tsx | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/charts/index.ts b/app/charts/index.ts index 370e0f616..b4a90492c 100644 --- a/app/charts/index.ts +++ b/app/charts/index.ts @@ -287,10 +287,12 @@ const META: Meta = { }; export const getInitialConfig = ({ + key, chartType, dimensions, measures, }: { + key?: string; chartType: ChartType; dimensions: DataCubeMetadataWithHierarchies["dimensions"]; measures: DataCubeMetadataWithHierarchies["measures"]; @@ -301,7 +303,7 @@ export const getInitialConfig = ({ meta: Meta; activeField: string | undefined; } = { - key: createChartId(), + key: key ?? createChartId(), version: CHART_CONFIG_VERSION, meta: META, activeField: undefined, @@ -510,6 +512,7 @@ export const getChartConfigAdjustedToChartType = ({ }): ChartConfig => { const oldChartType = chartConfig.chartType; const initialConfig = getInitialConfig({ + key: chartConfig.key, chartType: newChartType, dimensions, measures, diff --git a/app/configurator/configurator-state.tsx b/app/configurator/configurator-state.tsx index f2358825c..ebb9d72a9 100644 --- a/app/configurator/configurator-state.tsx +++ b/app/configurator/configurator-state.tsx @@ -905,7 +905,9 @@ const reducer: Reducer = ( return draft; case "DATASOURCE_CHANGED": draft.dataSource = action.value; + return draft; + case "CHART_TYPE_CHANGED": if (draft.state === "CONFIGURING_CHART") { const { locale, chartType } = action.value; From 4eb01d79e974037ca91655d7a998845099d52287 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Tue, 5 Sep 2023 12:15:34 +0200 Subject: [PATCH 05/40] chore: Remove console.logs --- app/charts/column/columns-stacked-state.tsx | 1 - app/charts/line/lines-state.tsx | 1 - app/components/chart-preview.tsx | 1 - 3 files changed, 3 deletions(-) diff --git a/app/charts/column/columns-stacked-state.tsx b/app/charts/column/columns-stacked-state.tsx index 7ccdc3dbe..ec167ee8e 100644 --- a/app/charts/column/columns-stacked-state.tsx +++ b/app/charts/column/columns-stacked-state.tsx @@ -85,7 +85,6 @@ const useColumnsStackedState = ( data: ColumnsStackedStateData ): StackedColumnsState => { const { aspectRatio, chartConfig } = chartProps; - console.log(chartConfig); const { xDimension, getX, diff --git a/app/charts/line/lines-state.tsx b/app/charts/line/lines-state.tsx index d1d0deeee..623cb3ed5 100644 --- a/app/charts/line/lines-state.tsx +++ b/app/charts/line/lines-state.tsx @@ -70,7 +70,6 @@ const useLinesState = ( data: ChartStateData ): LinesState => { const { chartConfig, aspectRatio } = chartProps; - console.log(chartConfig); const { xDimension, getX, diff --git a/app/components/chart-preview.tsx b/app/components/chart-preview.tsx index 43dfb4325..9b0e50768 100644 --- a/app/components/chart-preview.tsx +++ b/app/components/chart-preview.tsx @@ -65,7 +65,6 @@ export const ChartPreviewInner = (props: ChartPreviewProps) => { const { dataSetIri, dataSource } = props; const [state, dispatch] = useConfiguratorState(); const chartConfig = getChartConfig(state); - console.log(state.activeChartKey, chartConfig); const locale = useLocale(); const classes = useStyles(); const [{ data: metadata }] = useDataCubeMetadataQuery({ From 565cf0addb8c82381f99d1c915b4dafb7155cb9e Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Tue, 5 Sep 2023 12:26:00 +0200 Subject: [PATCH 06/40] feat: Highlight active chart in tabs --- app/components/chart-selection-tabs.tsx | 59 ++++++++++++++++--------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/app/components/chart-selection-tabs.tsx b/app/components/chart-selection-tabs.tsx index ed8e4212b..a9e37b14f 100644 --- a/app/components/chart-selection-tabs.tsx +++ b/app/components/chart-selection-tabs.tsx @@ -71,11 +71,7 @@ export const ChartSelectionTabs = ({ }) => { return ( - {editable ? ( - - ) : ( - - )} + {editable ? : } ); }; @@ -86,22 +82,20 @@ const useStyles = makeStyles((theme) => ({ padding: `0 ${theme.spacing(3)} ${theme.spacing(3)}`, }, tabContent: { - gap: theme.spacing(2), + gap: theme.spacing(1), alignItems: "center", - padding: `${theme.spacing(1)} ${theme.spacing(3)}`, + padding: theme.spacing(2), borderRadius: 3, transition: "0.125s ease background-color", - "&:hover": { - backgroundColor: ({ editable }: { editable: boolean }) => - editable ? theme.palette.grey[200] : undefined, - }, + cursor: "default", }, tabContentIconContainer: { + minWidth: "fit-content", color: theme.palette.grey[700], }, })); -const TabsEditable = ({ chartType }: { chartType: ChartType }) => { +const TabsEditable = () => { const [state, dispatch] = useConfiguratorState(isConfiguring); const chartConfig = getChartConfig(state); const [tabsState, setTabsState] = useTabsState(); @@ -129,6 +123,7 @@ const TabsEditable = ({ chartType }: { chartType: ChartType }) => { key: d.key, chartType: d.chartType, editable: true, + active: d.key === chartConfig.key, }; })} onActionButtonClick={(e: React.MouseEvent) => { @@ -174,7 +169,7 @@ const TabsEditable = ({ chartType }: { chartType: ChartType }) => { }; const TabsFixed = ({ chartType }: { chartType: ChartType }) => { - return ; + return ; }; const PublishChartButton = () => { @@ -230,7 +225,12 @@ const TabsInner = ({ onSwitchButtonClick, onAddButtonClick, }: { - data: { key: string; chartType: ChartType; editable?: boolean }[]; + data: { + key: string; + chartType: ChartType; + editable?: boolean; + active: boolean; + }[]; onActionButtonClick?: (e: React.MouseEvent) => void; onSwitchButtonClick?: (key: string) => void; onAddButtonClick?: () => void; @@ -246,6 +246,7 @@ const TabsInner = ({ { e.stopPropagation(); @@ -272,9 +274,10 @@ const TabsInner = ({ border: "1px solid", borderBottomWidth: 0, borderColor: "divider", + minWidth: "fit-content", }} onClick={onAddButtonClick} - label={} + label={} /> @@ -285,11 +288,13 @@ const TabsInner = ({ const TabContent = ({ iconName, editable, + active, onEditClick, onSwitchClick, }: { iconName: IconName; editable: boolean; + active: boolean; onEditClick?: (e: React.MouseEvent) => void; onSwitchClick?: (e: React.MouseEvent) => void; }) => { @@ -297,14 +302,28 @@ const TabContent = ({ return ( - {editable && ( - )} From fd9698dd4c617d06d0c845fe2ebb2dccf09b5f3e Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Tue, 5 Sep 2023 12:28:31 +0200 Subject: [PATCH 07/40] fix: Use chart meta when displaying title and subtitle --- app/components/chart-preview.tsx | 20 +++++++++++-------- .../components/chart-annotator.tsx | 8 +++++--- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/app/components/chart-preview.tsx b/app/components/chart-preview.tsx index 9b0e50768..1a9e6656d 100644 --- a/app/components/chart-preview.tsx +++ b/app/components/chart-preview.tsx @@ -142,7 +142,9 @@ export const ChartPreviewInner = (props: ChartPreviewProps) => { variant="h2" sx={{ color: - state.meta.title[locale] === "" ? "grey.500" : "text", + chartConfig.meta.title[locale] === "" + ? "grey.500" + : "text", }} className={classes.title} onClick={() => @@ -152,10 +154,10 @@ export const ChartPreviewInner = (props: ChartPreviewProps) => { }) } > - {state.meta.title[locale] === "" ? ( + {chartConfig.meta.title[locale] === "" ? ( [ Title ] ) : ( - state.meta.title[locale] + chartConfig.meta.title[locale] )} @@ -168,9 +170,9 @@ export const ChartPreviewInner = (props: ChartPreviewProps) => { - {state.meta.title[locale] === "" + {chartConfig.meta.title[locale] === "" ? metadata?.dataCubeByIri?.title - : state.meta.title[locale]}{" "} + : chartConfig.meta.title[locale]}{" "} - visualize.admin.ch @@ -179,7 +181,9 @@ export const ChartPreviewInner = (props: ChartPreviewProps) => { className={classes.description} sx={{ color: - state.meta.description[locale] === "" ? "grey.500" : "text", + chartConfig.meta.description[locale] === "" + ? "grey.500" + : "text", }} onClick={() => dispatch({ @@ -188,10 +192,10 @@ export const ChartPreviewInner = (props: ChartPreviewProps) => { }) } > - {state.meta.description[locale] === "" ? ( + {chartConfig.meta.description[locale] === "" ? ( [ Description ] ) : ( - state.meta.description[locale] + chartConfig.meta.description[locale] )} diff --git a/app/configurator/components/chart-annotator.tsx b/app/configurator/components/chart-annotator.tsx index ce67e378b..b8ae1894e 100644 --- a/app/configurator/components/chart-annotator.tsx +++ b/app/configurator/components/chart-annotator.tsx @@ -1,6 +1,7 @@ import { Trans, t } from "@lingui/macro"; import * as React from "react"; +import { getChartConfig } from "@/config-types"; import { ControlSection, ControlSectionContent, @@ -14,7 +15,8 @@ import { isConfiguring } from "../configurator-state"; export const TitleAndDescriptionConfigurator = () => { const [state] = useConfiguratorState(isConfiguring); - const { title, description } = state.meta; + const chartConfig = getChartConfig(state); + const { title, description } = chartConfig.meta; const locale = useLocale(); const disabled = React.useMemo(() => { return !(title[locale] !== "" && description[locale] !== ""); @@ -44,7 +46,7 @@ export const TitleAndDescriptionConfigurator = () => { @@ -54,7 +56,7 @@ export const TitleAndDescriptionConfigurator = () => { mainLabel={getFieldLabel("title")} /> From 9f45fe128d244061544bab77fc417de84d1a95d0 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Tue, 5 Sep 2023 12:48:07 +0200 Subject: [PATCH 08/40] fix: Tests --- app/charts/index.spec.ts | 16 +++++ .../use-sync-interactive-filters.spec.tsx | 16 +++-- app/configurator/configurator-state.spec.tsx | 70 +++++++++++++------ app/docs/fixtures.ts | 3 + 4 files changed, 81 insertions(+), 24 deletions(-) diff --git a/app/charts/index.spec.ts b/app/charts/index.spec.ts index 2bdec8a6d..7f144c183 100644 --- a/app/charts/index.spec.ts +++ b/app/charts/index.spec.ts @@ -78,9 +78,24 @@ describe("possible chart types", () => { describe("chart type switch", () => { it("should correctly remove non-allowed interactive data filters", () => { const chartConfig: ColumnConfig = { + key: "column", version: "1.4.0", chartType: "column", filters: {}, + meta: { + title: { + en: "", + de: "", + fr: "", + it: "", + }, + description: { + en: "", + de: "", + fr: "", + it: "", + }, + }, fields: { x: { componentIri: @@ -115,6 +130,7 @@ describe("chart type switch", () => { type: "identity", }, }, + activeField: undefined, }; const newConfig = getChartConfigAdjustedToChartType({ chartConfig, diff --git a/app/charts/shared/use-sync-interactive-filters.spec.tsx b/app/charts/shared/use-sync-interactive-filters.spec.tsx index 66fd21759..e7add50dc 100644 --- a/app/charts/shared/use-sync-interactive-filters.spec.tsx +++ b/app/charts/shared/use-sync-interactive-filters.spec.tsx @@ -6,6 +6,7 @@ import useSyncInteractiveFilters from "@/charts/shared/use-sync-interactive-filt import { ChartConfig, InteractiveFiltersConfig } from "@/config-types"; import { useInteractiveFiltersStore } from "@/stores/interactive-filters"; import fixture from "@/test/__fixtures/config/dev/4YL1p4QTFQS4.json"; +import { migrateChartConfig } from "@/utils/chart-config/versioning"; const interactiveFiltersConfig: InteractiveFiltersConfig = { legend: { @@ -33,10 +34,17 @@ const interactiveFiltersConfig: InteractiveFiltersConfig = { }, }; -const chartConfig = { - ...fixture.data.chartConfig, - interactiveFiltersConfig, -} as ChartConfig; +const chartConfig = migrateChartConfig( + { + ...fixture.data.chartConfig, + interactiveFiltersConfig, + }, + { + migrationProps: { + meta: {}, + }, + } +) as ChartConfig; const setup = ({ modifiedChartConfig, diff --git a/app/configurator/configurator-state.spec.tsx b/app/configurator/configurator-state.spec.tsx index eab5a5345..d50b0c4c5 100644 --- a/app/configurator/configurator-state.spec.tsx +++ b/app/configurator/configurator-state.spec.tsx @@ -11,6 +11,7 @@ import { DataSource, MapConfig, TableConfig, + getChartConfig, } from "@/config-types"; import { applyNonTableDimensionToFilters, @@ -37,7 +38,10 @@ import { data as fakeVizFixture } from "@/test/__fixtures/config/prod/line-1.jso import bathingWaterMetadata from "@/test/__fixtures/data/DataCubeMetadataWithComponentValues-bathingWater.json"; import covid19Metadata from "@/test/__fixtures/data/DataCubeMetadataWithComponentValues-covid19.json"; import * as api from "@/utils/chart-config/api"; -import { migrateChartConfig } from "@/utils/chart-config/versioning"; +import { + migrateChartConfig, + migrateConfiguratorState, +} from "@/utils/chart-config/versioning"; const mockedApi = api as jest.Mocked; @@ -100,8 +104,23 @@ describe("initChartStateFromChart", () => { data: fakeVizFixture, }, }); - const state = await initChartStateFromChart("abcde"); - expect(state).toEqual(expect.objectContaining(fakeVizFixture)); + // @ts-ignore + const { key, activeChartKey, chartConfigs, ...rest } = + await initChartStateFromChart("abcde"); + const { key: chartConfigKey, ...chartConfig } = chartConfigs[0]; + const { + key: migratedKey, + activeChartKey: migratedActiveChartKey, + chartConfigs: migratedChartsConfigs, + ...migratedRest + } = migrateConfiguratorState(fakeVizFixture); + const { key: migratedChartConfigKey, ...migratedChartConfig } = + migratedChartsConfigs[0]; + + expect(rest).toEqual( + expect.objectContaining(migrateConfiguratorState(migratedRest)) + ); + expect(chartConfig).toEqual(migratedChartConfig); }); it("should return undefined if chart is invalid", async () => { @@ -708,7 +727,12 @@ describe("retainChartConfigWhenSwitchingChartType", () => { }; it("should retain appropriate x & y fields and discard the others", () => { - runChecks(migrateChartConfig(covid19ColumnChartConfig), xyChecks); + runChecks( + migrateChartConfig(covid19ColumnChartConfig, { + migrationProps: { meta: {} }, + }), + xyChecks + ); }); it("should retain appropriate segment fields and discard the others", () => { @@ -744,26 +768,30 @@ describe("colorMapping", () => { it("should correctly reset color mapping", () => { const state = { state: "CONFIGURING_CHART", - chartConfig: { - fields: { - areaLayer: { - componentIri: "areaLayerIri", - color: { - type: "categorical", - componentIri: "areaLayerColorIri", - palette: "dimension", - colorMapping: { - red: "green", - green: "blue", - blue: "red", + chartConfigs: [ + { + key: "abc", + fields: { + areaLayer: { + componentIri: "areaLayerIri", + color: { + type: "categorical", + componentIri: "areaLayerColorIri", + palette: "dimension", + colorMapping: { + red: "green", + green: "blue", + blue: "red", + }, }, }, }, }, - }, - }; + ], + activeChartKey: "abc", + } as unknown as ConfiguratorStateConfiguringChart; - updateColorMapping(state as unknown as ConfiguratorStateConfiguringChart, { + updateColorMapping(state, { type: "CHART_CONFIG_UPDATE_COLOR_MAPPING", value: { field: "areaLayer", @@ -778,7 +806,9 @@ describe("colorMapping", () => { }, }); - expect(state.chartConfig.fields.areaLayer.color.colorMapping).toEqual({ + expect( + (getChartConfig(state).fields as any).areaLayer.color.colorMapping + ).toEqual({ red: "red", green: "green", blue: "blue", diff --git a/app/docs/fixtures.ts b/app/docs/fixtures.ts index 8974155bc..a530ce581 100644 --- a/app/docs/fixtures.ts +++ b/app/docs/fixtures.ts @@ -1,4 +1,5 @@ import { DEFAULT_DATA_SOURCE } from "@/domain/datasource"; +import { CONFIGURATOR_STATE_VERSION } from "@/utils/chart-config/versioning"; import { ColumnFields, ConfiguratorState, TableConfig } from "../configurator"; import { DimensionMetadataFragment, TimeUnit } from "../graphql/query-hooks"; @@ -6,6 +7,7 @@ import { DimensionMetadataFragment, TimeUnit } from "../graphql/query-hooks"; export const states: ConfiguratorState[] = [ { state: "SELECTING_DATASET", + version: CONFIGURATOR_STATE_VERSION, dataSet: undefined, dataSource: DEFAULT_DATA_SOURCE, chartConfigs: undefined, @@ -27,6 +29,7 @@ export const states: ConfiguratorState[] = [ }, { state: "CONFIGURING_CHART", + version: CONFIGURATOR_STATE_VERSION, dataSet: "foo", dataSource: DEFAULT_DATA_SOURCE, chartConfigs: [ From e0838153578afc796831d14ce29a53f42b2768c1 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Tue, 5 Sep 2023 12:49:17 +0200 Subject: [PATCH 09/40] refactor: Use simpler method to replace chart config --- app/configurator/configurator-state.tsx | 47 ++++++++----------------- 1 file changed, 14 insertions(+), 33 deletions(-) diff --git a/app/configurator/configurator-state.tsx b/app/configurator/configurator-state.tsx index ebb9d72a9..aed0b6925 100644 --- a/app/configurator/configurator-state.tsx +++ b/app/configurator/configurator-state.tsx @@ -778,15 +778,8 @@ export const handleChartFieldChanged = ( } const newConfig = deriveFiltersFromFields(chartConfig, dimensions); - - for (const k in chartConfig) { - if (chartConfig.hasOwnProperty(k)) { - // @ts-ignore - delete chartConfig[k]; - } - } - - Object.assign(chartConfig, newConfig); + const index = draft.chartConfigs.findIndex((d) => d.key === chartConfig.key); + draft.chartConfigs[index] = newConfig; return draft; }; @@ -915,22 +908,18 @@ const reducer: Reducer = ( if (metadata) { const { dimensions, measures } = metadata; - const previousConfig = getChartConfig(draft); + const chartConfig = getChartConfig(draft); const newConfig = getChartConfigAdjustedToChartType({ - chartConfig: current(previousConfig), + chartConfig: current(chartConfig), newChartType: chartType, dimensions, measures, }); - for (const k in previousConfig) { - if (previousConfig.hasOwnProperty(k)) { - // @ts-ignore - delete previousConfig[k]; - } - } - - Object.assign(previousConfig, newConfig); + const index = draft.chartConfigs.findIndex( + (d) => d.key === chartConfig.key + ); + draft.chartConfigs[index] = newConfig; } } @@ -1050,20 +1039,12 @@ const reducer: Reducer = ( case "CHART_CONFIG_REPLACED": if (draft.state === "CONFIGURING_CHART") { const chartConfig = getChartConfig(draft); - - for (const k in chartConfig) { - if (chartConfig.hasOwnProperty(k)) { - // @ts-ignore - delete chartConfig[k]; - } - } - - Object.assign( - chartConfig, - deriveFiltersFromFields( - action.value.chartConfig, - action.value.dataSetMetadata.dimensions - ) + const index = draft.chartConfigs.findIndex( + (d) => d.key === chartConfig.key + ); + draft.chartConfigs[index] = deriveFiltersFromFields( + action.value.chartConfig, + action.value.dataSetMetadata.dimensions ); } From ba50513994cffddfcf56922e76a77e05643a0643 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Tue, 5 Sep 2023 13:02:04 +0200 Subject: [PATCH 10/40] fix: Change correct chart (selection tabs) --- app/components/chart-selection-tabs.tsx | 40 +++++++++++++++---- .../components/chart-configurator.tsx | 7 +++- .../components/chart-type-selector.tsx | 5 ++- app/configurator/config-form.tsx | 7 +++- app/configurator/configurator-state.tsx | 5 ++- .../table/table-chart-configurator.tsx | 7 +++- 6 files changed, 57 insertions(+), 14 deletions(-) diff --git a/app/components/chart-selection-tabs.tsx b/app/components/chart-selection-tabs.tsx index a9e37b14f..6407137f3 100644 --- a/app/components/chart-selection-tabs.tsx +++ b/app/components/chart-selection-tabs.tsx @@ -33,6 +33,7 @@ import Flex from "./flex"; type TabsState = { isPopoverOpen: boolean; + activeChartKey?: string; }; const TabsStateContext = createContext< @@ -107,7 +108,10 @@ const TabsEditable = () => { const handleClose = useEvent(() => { setPopoverAnchorEl(null); - setTabsState({ isPopoverOpen: false }); + setTabsState({ + isPopoverOpen: false, + activeChartKey: tabsState.activeChartKey, + }); }); useEffect(() => { @@ -126,10 +130,13 @@ const TabsEditable = () => { active: d.key === chartConfig.key, }; })} - onActionButtonClick={(e: React.MouseEvent) => { + onActionButtonClick={( + e: React.MouseEvent, + activeChartKey: string + ) => { e.stopPropagation(); setPopoverAnchorEl(e.currentTarget); - setTabsState({ isPopoverOpen: true }); + setTabsState({ isPopoverOpen: true, activeChartKey }); }} onSwitchButtonClick={(key: string) => { dispatch({ @@ -162,6 +169,7 @@ const TabsEditable = () => { @@ -231,7 +239,10 @@ const TabsInner = ({ editable?: boolean; active: boolean; }[]; - onActionButtonClick?: (e: React.MouseEvent) => void; + onActionButtonClick?: ( + e: React.MouseEvent, + activeChartKey: string + ) => void; onSwitchButtonClick?: (key: string) => void; onAddButtonClick?: () => void; }) => { @@ -256,6 +267,7 @@ const TabsInner = ({ label={ } + label={ + + } /> @@ -287,15 +306,20 @@ const TabsInner = ({ const TabContent = ({ iconName, + chartKey, editable, active, onEditClick, onSwitchClick, }: { iconName: IconName; + chartKey: string; editable: boolean; active: boolean; - onEditClick?: (e: React.MouseEvent) => void; + onEditClick?: ( + e: React.MouseEvent, + activeChartKey: string + ) => void; onSwitchClick?: (e: React.MouseEvent) => void; }) => { const classes = useStyles({ editable }); @@ -320,7 +344,9 @@ const TabContent = ({ {editable && ( ); }; From e7a50818e79a358d30239857bca05e9a17bb4b5a Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Wed, 6 Sep 2023 11:11:30 +0200 Subject: [PATCH 15/40] refactor: Active chart key is always defined from certain point --- app/config-types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/config-types.ts b/app/config-types.ts index 7f7fd06d2..085cdb3a1 100644 --- a/app/config-types.ts +++ b/app/config-types.ts @@ -919,7 +919,7 @@ const Config = t.type( dataSource: DataSource, meta: Meta, chartConfigs: t.array(ChartConfig), - activeChartKey: t.union([t.string, t.undefined]), + activeChartKey: t.string, }, "Config" ); From c83dc2d57044531c9984aaeb4a4f1f55ce17af91 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Wed, 6 Sep 2023 11:12:47 +0200 Subject: [PATCH 16/40] feat: Store configurator state version in the db --- app/pages/api/config.ts | 4 ++-- app/utils/chart-config/api.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/pages/api/config.ts b/app/pages/api/config.ts index d4911bcdd..b7f2ae0f7 100644 --- a/app/pages/api/config.ts +++ b/app/pages/api/config.ts @@ -9,11 +9,11 @@ const route = api({ const session = await getServerSideSession(req, res); const userId = session?.user?.id; const { data } = req.body; - const result = await createConfig({ + + return await createConfig({ data, userId, }); - return result; }, }); diff --git a/app/utils/chart-config/api.ts b/app/utils/chart-config/api.ts index 12eb9fc92..ab366b6d0 100644 --- a/app/utils/chart-config/api.ts +++ b/app/utils/chart-config/api.ts @@ -11,6 +11,7 @@ export const createConfig = async (state: ConfiguratorStatePublishing) => { method: "POST", data: { data: { + version: state.version, dataSet: state.dataSet, dataSource: state.dataSource, meta: state.meta, From 31a6dc3e5970f72efb56e6272fa3328c3506e98f Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Wed, 6 Sep 2023 11:12:59 +0200 Subject: [PATCH 17/40] refactor: Remove unused props --- app/components/chart-panel.tsx | 38 ++++++++++++---------------------- 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/app/components/chart-panel.tsx b/app/components/chart-panel.tsx index e73b12f05..aeef0d3f5 100644 --- a/app/components/chart-panel.tsx +++ b/app/components/chart-panel.tsx @@ -1,34 +1,28 @@ -import { BoxProps, Theme } from "@mui/material"; +import { Theme } from "@mui/material"; import { makeStyles } from "@mui/styles"; -import { ReactNode } from "react"; +import React from "react"; +import { ChartSelectionTabs } from "@/components/chart-selection-tabs"; import Flex from "@/components/flex"; -import { ChartType } from "@/configurator"; -import { ChartSelectionTabs } from "./chart-selection-tabs"; +export const ChartPanelConfigurator = (props: React.PropsWithChildren<{}>) => { + const { children } = props; -type ChartPanelProps = { - children: ReactNode; -} & BoxProps; - -export const ChartPanelConfigurator = (props: ChartPanelProps) => { return ( <> - + {children} ); }; -export const ChartPanelPublished = ( - props: ChartPanelProps & { - chartType: ChartType; - } -) => { +export const ChartPanelPublished = (props: React.PropsWithChildren<{}>) => { + const { children } = props; + return ( <> - + {children} ); }; @@ -44,18 +38,12 @@ const useChartPanelInnerStyles = makeStyles((theme) => ({ }, })); -const ChartPanelInner = ({ children, ...boxProps }: ChartPanelProps) => { +const ChartPanelInner = (props: React.PropsWithChildren<{}>) => { + const { children } = props; const classes = useChartPanelInnerStyles(); return ( - + {children} ); From fbc7f78b8eb0a3f44d9f01be658e4e64d435bfea Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Wed, 6 Sep 2023 11:48:05 +0200 Subject: [PATCH 18/40] feat: Do not show ChartSelectionTabs for single chart in published mode --- app/components/chart-selection-tabs.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/components/chart-selection-tabs.tsx b/app/components/chart-selection-tabs.tsx index 9571eade7..ef1df294f 100644 --- a/app/components/chart-selection-tabs.tsx +++ b/app/components/chart-selection-tabs.tsx @@ -71,6 +71,12 @@ export const ChartSelectionTabs = ({ ConfiguratorStateConfiguringChart | ConfiguratorStatePublishing, Dispatch ]; + const singleConfig = state.chartConfigs.length === 1; + + if (singleConfig) { + return null; + } + const chartConfig = getChartConfig(state); const data: TabDatum[] = state.chartConfigs.map((d) => { return { From f89efb848adadbd7c1b8458a5d7149ec74f0e80c Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Wed, 6 Sep 2023 12:01:50 +0200 Subject: [PATCH 19/40] fix: Initialize a new chart in editing mode with CONFIGURING_CHART state --- app/configurator/configurator-state.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/configurator/configurator-state.tsx b/app/configurator/configurator-state.tsx index 6094ee66a..7ffc47183 100644 --- a/app/configurator/configurator-state.tsx +++ b/app/configurator/configurator-state.tsx @@ -1265,11 +1265,14 @@ type DatasetIri = string; export const initChartStateFromChart = async ( from: ChartId -): Promise => { +): Promise => { const config = await fetchChartConfig(from); if (config?.data) { - return migrateConfiguratorState(config.data); + return migrateConfiguratorState({ + ...config.data, + state: "CONFIGURING_CHART", + }); } }; From 1c01783f0e14c99ab0a5cbd9f04e6cfcaff3e693 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Wed, 6 Sep 2023 13:28:48 +0200 Subject: [PATCH 20/40] fix: Store chartConfigs as array, not object --- app/configurator/configurator-state.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/configurator/configurator-state.tsx b/app/configurator/configurator-state.tsx index 7ffc47183..4330ba3c6 100644 --- a/app/configurator/configurator-state.tsx +++ b/app/configurator/configurator-state.tsx @@ -1440,7 +1440,7 @@ const ConfiguratorStateProviderInternal = ({ try { const result = await createConfig({ ...state, - chartConfigs: { + chartConfigs: [ ...state.chartConfigs.map((d) => { return { ...d, @@ -1454,7 +1454,7 @@ const ConfiguratorStateProviderInternal = ({ ), }; }), - }, + ], }); /** From cd4b5b0d761ffe5c8855c9bef7bcafeb3b50ce5a Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Wed, 6 Sep 2023 13:29:07 +0200 Subject: [PATCH 21/40] feat: Store activeChartKey in db --- app/configurator/configurator-state.tsx | 1 + app/utils/chart-config/api.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/app/configurator/configurator-state.tsx b/app/configurator/configurator-state.tsx index 4330ba3c6..ea8261534 100644 --- a/app/configurator/configurator-state.tsx +++ b/app/configurator/configurator-state.tsx @@ -1455,6 +1455,7 @@ const ConfiguratorStateProviderInternal = ({ }; }), ], + activeChartKey: state.chartConfigs[0].key, }); /** diff --git a/app/utils/chart-config/api.ts b/app/utils/chart-config/api.ts index ab366b6d0..22bee59cf 100644 --- a/app/utils/chart-config/api.ts +++ b/app/utils/chart-config/api.ts @@ -16,6 +16,7 @@ export const createConfig = async (state: ConfiguratorStatePublishing) => { dataSource: state.dataSource, meta: state.meta, chartConfigs: state.chartConfigs, + activeChartKey: state.activeChartKey, }, }, }); From 41ed59cb5b93929235dc2b6abed281a99194adb2 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Wed, 6 Sep 2023 13:56:03 +0200 Subject: [PATCH 22/40] feat: Add PUBLISHED configurator state ...as we need to interact with it in published mode when switching active chart type. --- app/components/chart-selection-tabs.tsx | 17 ++++++++++++----- app/config-types.ts | 11 +++++++++++ app/configurator/configurator-state.tsx | 25 ++++++++++++++++++++----- app/docs/fixtures.ts | 2 +- app/docs/lines.docs.tsx | 20 ++++++++++++-------- app/docs/scatterplot.docs.tsx | 20 ++++++++++++-------- 6 files changed, 68 insertions(+), 27 deletions(-) diff --git a/app/components/chart-selection-tabs.tsx b/app/components/chart-selection-tabs.tsx index ef1df294f..c2f1d7dc1 100644 --- a/app/components/chart-selection-tabs.tsx +++ b/app/components/chart-selection-tabs.tsx @@ -15,6 +15,7 @@ import { ChartConfig, ChartType, ConfiguratorStateConfiguringChart, + ConfiguratorStatePublished, ConfiguratorStatePublishing, getChartConfig, useConfiguratorState, @@ -68,10 +69,13 @@ export const ChartSelectionTabs = ({ editable: boolean; }) => { const [state] = useConfiguratorState() as [ - ConfiguratorStateConfiguringChart | ConfiguratorStatePublishing, + ( + | ConfiguratorStateConfiguringChart + | ConfiguratorStatePublishing + | ConfiguratorStatePublished + ), Dispatch ]; - const singleConfig = state.chartConfigs.length === 1; if (singleConfig) { return null; @@ -118,7 +122,10 @@ const useStyles = makeStyles((theme) => ({ })); type TabsEditableProps = { - state: ConfiguratorStateConfiguringChart | ConfiguratorStatePublishing; + state: + | ConfiguratorStateConfiguringChart + | ConfiguratorStatePublishing + | ConfiguratorStatePublished; chartConfig: ChartConfig; data: TabDatum[]; }; @@ -215,8 +222,8 @@ const TabsFixed = (props: TabsFixedProps) => { const PublishChartButton = () => { const [state, dispatch] = useConfiguratorState(); const { dataSet: dataSetIri } = state as - | ConfiguratorStatePublishing - | ConfiguratorStateConfiguringChart; + | ConfiguratorStateConfiguringChart + | ConfiguratorStatePublishing; const locale = useLocale(); const [{ data: metadata }] = useDataCubeMetadataQuery({ variables: { diff --git a/app/config-types.ts b/app/config-types.ts index 085cdb3a1..a905ba1ad 100644 --- a/app/config-types.ts +++ b/app/config-types.ts @@ -976,11 +976,22 @@ export type ConfiguratorStatePublishing = t.TypeOf< typeof ConfiguratorStatePublishing >; +const ConfiguratorStatePublished = t.intersection([ + t.type({ + state: t.literal("PUBLISHED"), + }), + Config, +]); +export type ConfiguratorStatePublished = t.TypeOf< + typeof ConfiguratorStatePublished +>; + const ConfiguratorState = t.union([ ConfiguratorStateInitial, ConfiguratorStateSelectingDataSet, ConfiguratorStateConfiguringChart, ConfiguratorStatePublishing, + ConfiguratorStatePublished, ]); export type ConfiguratorState = t.TypeOf; diff --git a/app/configurator/configurator-state.tsx b/app/configurator/configurator-state.tsx index ea8261534..42587f895 100644 --- a/app/configurator/configurator-state.tsx +++ b/app/configurator/configurator-state.tsx @@ -36,6 +36,7 @@ import { ColumnStyleCategory, ConfiguratorState, ConfiguratorStateConfiguringChart, + ConfiguratorStatePublished, ConfiguratorStateSelectingDataSet, DataSource, FilterValue, @@ -98,7 +99,10 @@ export type ConfiguratorStateAction = } | { type: "STEP_PREVIOUS"; - to?: Exclude; + to?: Exclude< + ConfiguratorState["state"], + "INITIAL" | "PUBLISHING" | "PUBLISHED" + >; } | { type: "DATASET_SELECTED"; @@ -622,6 +626,7 @@ const transitionStepNext = ( case "INITIAL": case "PUBLISHING": + case "PUBLISHED": break; default: throw unreachableError(draft); @@ -1245,7 +1250,7 @@ const reducer: Reducer = ( return draft; case "SWITCH_ACTIVE_CHART": - if (draft.state === "CONFIGURING_CHART") { + if (draft.state === "CONFIGURING_CHART" || draft.state === "PUBLISHED") { draft.activeChartKey = action.value; } @@ -1368,13 +1373,14 @@ const ConfiguratorStateProviderInternal = ({ const { asPath, push, replace, query } = useRouter(); const client = useClient(); - // Re-initialize state on page load + // Initialize state on page load. useEffect(() => { let stateToInitialize = initialStateWithDataSource; const initialize = async () => { try { let newChartState; + if (chartId === "new") { if (query.from && typeof query.from === "string") { newChartState = await initChartStateFromChart(query.from); @@ -1386,16 +1392,17 @@ const ConfiguratorStateProviderInternal = ({ locale ); } - } else { + } else if (chartId !== "published") { newChartState = await initChartStateFromLocalStorage(chartId); if (!newChartState && allowDefaultRedirect) replace(`/create/new`); } - stateToInitialize = newChartState || stateToInitialize; + stateToInitialize = newChartState ?? stateToInitialize; } finally { dispatch({ type: "INITIALIZED", value: stateToInitialize }); } }; + initialize(); }, [ dataSource, @@ -1434,6 +1441,7 @@ const ConfiguratorStateProviderInternal = ({ JSON.stringify(state) ); } + return; case "PUBLISHING": (async () => { @@ -1481,6 +1489,7 @@ const ConfiguratorStateProviderInternal = ({ dispatch({ type: "PUBLISH_FAILED" }); } })(); + return; } } catch (e) { @@ -1591,3 +1600,9 @@ export const isConfiguring = ( ): s is ConfiguratorStateConfiguringChart => { return s.state === "CONFIGURING_CHART"; }; + +export const isPublished = ( + s: ConfiguratorState +): s is ConfiguratorStatePublished => { + return s.state === "PUBLISHED"; +}; diff --git a/app/docs/fixtures.ts b/app/docs/fixtures.ts index a530ce581..bd7e1108d 100644 --- a/app/docs/fixtures.ts +++ b/app/docs/fixtures.ts @@ -101,7 +101,7 @@ export const states: ConfiguratorState[] = [ en: "", }, }, - activeChartKey: undefined, + activeChartKey: "column", }, ]; diff --git a/app/docs/lines.docs.tsx b/app/docs/lines.docs.tsx index 8b6c06b45..32c570c24 100644 --- a/app/docs/lines.docs.tsx +++ b/app/docs/lines.docs.tsx @@ -13,7 +13,7 @@ import { LineConfig, SortingField, } from "@/configurator"; -import { PublishedConfiguratorStateProvider } from "@/configurator/configurator-state"; +import { ConfiguratorStateProvider } from "@/configurator/configurator-state"; import { DimensionMetadataFragment } from "@/graphql/query-hooks"; import { CHART_CONFIG_VERSION } from "@/utils/chart-config/versioning"; @@ -23,15 +23,19 @@ export const Docs = () => markdown` ${( - )} - + )} `; diff --git a/app/docs/scatterplot.docs.tsx b/app/docs/scatterplot.docs.tsx index 73a09087a..45f990b10 100644 --- a/app/docs/scatterplot.docs.tsx +++ b/app/docs/scatterplot.docs.tsx @@ -16,7 +16,7 @@ import { Tooltip } from "@/charts/shared/interaction/tooltip"; import { LegendColor } from "@/charts/shared/legend-color"; import { InteractionVoronoi } from "@/charts/shared/overlay-voronoi"; import { InteractiveFiltersConfig, ScatterPlotConfig } from "@/config-types"; -import { PublishedConfiguratorStateProvider } from "@/configurator/configurator-state"; +import { ConfiguratorStateProvider } from "@/configurator/configurator-state"; import { DimensionMetadataFragment } from "@/graphql/query-hooks"; import { CHART_CONFIG_VERSION } from "@/utils/chart-config/versioning"; @@ -26,15 +26,19 @@ export const Docs = () => markdown` ${( - )} - + )} `; From ed4190608aaea715122944a76e3e09107acfefc3 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Wed, 6 Sep 2023 15:26:45 +0200 Subject: [PATCH 23/40] refactor: Use configurator state in published charts ...and fix publishing of multiple charts at once --- app/components/chart-published.tsx | 68 ++--- app/components/chart-selection-tabs.tsx | 72 +++--- app/configurator/configurator-state.tsx | 29 +-- app/docs/chart-selection-tabs.docs.tsx | 15 +- app/docs/dataset-browse.docs.tsx | 6 +- app/homepage/examples.tsx | 323 +++++++++++------------- app/pages/__test/[env]/[slug].tsx | 38 +-- app/pages/_charts.tsx | 98 ++++--- app/pages/browse/index.tsx | 9 +- app/pages/create/[chartId].tsx | 6 +- app/pages/embed/[chartId].tsx | 33 ++- app/pages/v/[chartId].tsx | 60 +++-- app/src/index.ts | 2 +- 13 files changed, 365 insertions(+), 394 deletions(-) diff --git a/app/components/chart-published.tsx b/app/components/chart-published.tsx index 19be75289..7e8143377 100644 --- a/app/components/chart-published.tsx +++ b/app/components/chart-published.tsx @@ -23,10 +23,10 @@ import { } from "@/components/metadata-panel"; import { ChartConfig, - ConfiguratorStatePublishing, DataSource, - Meta, - PublishedConfiguratorStateProvider, + getChartConfig, + isPublished, + useConfiguratorState, } from "@/configurator"; import { DRAWER_WIDTH } from "@/configurator/components/drawer"; import { @@ -43,22 +43,20 @@ import { useEmbedOptions } from "@/utils/embed"; import useEvent from "@/utils/use-event"; type ChartPublishedProps = { - dataSet: string; - dataSource: DataSource; - meta: Meta; - chartConfig: ChartConfig; - configKey: string; + configKey?: string; }; export const ChartPublished = (props: ChartPublishedProps) => { - const { dataSet, dataSource, meta, chartConfig, configKey } = props; + const { configKey } = props; + const [state] = useConfiguratorState(isPublished); + const { dataSet, dataSource } = state; + const chartConfig = getChartConfig(state); return ( @@ -84,19 +82,18 @@ const useStyles = makeStyles((theme) => ({ type ChartPublishInnerProps = { dataSet: string; dataSource: DataSource | undefined; - meta: Meta; chartConfig: ChartConfig; - configKey: string; + configKey: string | undefined; }; -export const ChartPublishedInner = (props: ChartPublishInnerProps) => { +const ChartPublishedInner = (props: ChartPublishInnerProps) => { const { dataSet, dataSource = DEFAULT_DATA_SOURCE, - meta, chartConfig, configKey, } = props; + const { meta } = chartConfig; const rootRef = useRef(null); const { @@ -149,15 +146,6 @@ export const ChartPublishedInner = (props: ChartPublishInnerProps) => { componentIris: extractComponentIris(chartConfig), }, }); - - const publishedConfiguratorState = useMemo(() => { - return { - state: "PUBLISHING", - dataSet, - dataSource, - chartConfigs: [chartConfig], - } as ConfiguratorStatePublishing; - }, [dataSet, dataSource, chartConfig]); const handleToggleTableView = useEvent(() => setIsTablePreview((c) => !c)); const allComponents = useMemo(() => { @@ -249,25 +237,21 @@ export const ChartPublishedInner = (props: ChartPublishInnerProps) => { height={containerHeight.current!} flexGrow={1} > - - {isTablePreview ? ( - - ) : ( - - )} - + {isTablePreview ? ( + + ) : ( + + )} ]; - if (singleConfig) { + if (!editable && state.chartConfigs.length === 1) { return null; } @@ -86,7 +86,6 @@ export const ChartSelectionTabs = ({ return { key: d.key, chartType: d.chartType, - editable: true, active: d.key === chartConfig.key, }; }); @@ -157,6 +156,7 @@ const TabsEditable = (props: TabsEditableProps) => { <> , activeChartKey: string @@ -215,8 +215,20 @@ type TabsFixedProps = { const TabsFixed = (props: TabsFixedProps) => { const { data } = props; + const [, dispatch] = useConfiguratorState(); - return ; + return ( + { + dispatch({ + type: "SWITCH_ACTIVE_CHART", + value: key, + }); + }} + /> + ); }; const PublishChartButton = () => { @@ -268,16 +280,13 @@ const PublishChartButton = () => { const TabsInner = ({ data, + editable, onActionButtonClick, onSwitchButtonClick, onAddButtonClick, }: { - data: { - key: string; - chartType: ChartType; - editable?: boolean; - active: boolean; - }[]; + data: TabDatum[]; + editable: boolean; onActionButtonClick?: ( e: React.MouseEvent, activeChartKey: string @@ -302,12 +311,13 @@ const TabsInner = ({ border: "1px solid", borderBottomWidth: 0, borderColor: "divider", + minWidth: "fit-content", }} label={ { @@ -318,27 +328,29 @@ const TabsInner = ({ } /> ))} - - } - /> + {editable && ( + + } + /> + )} - + {editable && } ); }; diff --git a/app/configurator/configurator-state.tsx b/app/configurator/configurator-state.tsx index 42587f895..04d996c82 100644 --- a/app/configurator/configurator-state.tsx +++ b/app/configurator/configurator-state.tsx @@ -1504,34 +1504,7 @@ const ConfiguratorStateProviderInternal = ({ ); }; -export const PublishedConfiguratorStateProvider = ({ - children, - initialState, -}: { - children?: ReactNode; - initialState?: ConfiguratorState; -}) => { - const stateAndDispatch = useMemo(() => { - return [ - initialState, - () => { - throw new Error( - "Should not call dispatch on config statefor publishers" - ); - }, - ] as React.ComponentProps< - typeof ConfiguratorStateContext.Provider - >["value"]; - }, [initialState]); - - return ( - - {children} - - ); -}; - -export const EditorConfiguratorStateProvider = ({ +export const ConfiguratorStateProvider = ({ chartId, children, initialState, diff --git a/app/docs/chart-selection-tabs.docs.tsx b/app/docs/chart-selection-tabs.docs.tsx index 9ab0c7872..97abe271a 100644 --- a/app/docs/chart-selection-tabs.docs.tsx +++ b/app/docs/chart-selection-tabs.docs.tsx @@ -1,11 +1,10 @@ /* eslint-disable import/no-anonymous-default-export */ import { markdown, ReactSpecimen } from "catalog"; -import React from "react"; import { ChartSelectionTabs } from "@/components/chart-selection-tabs"; import { ConfiguratorStateConfiguringChart, - EditorConfiguratorStateProvider, + ConfiguratorStateProvider, } from "@/configurator"; import palmerPenguinsFixture from "@/test/__fixtures/config/int/scatterplot-palmer-penguins.json"; @@ -18,15 +17,15 @@ They can be either _editable_, to display a button to show ChartTypeSelector (us ${( - - - + + )} @@ -34,15 +33,15 @@ ${( ${( - - - + + )} diff --git a/app/docs/dataset-browse.docs.tsx b/app/docs/dataset-browse.docs.tsx index b6746a31c..5787f2241 100644 --- a/app/docs/dataset-browse.docs.tsx +++ b/app/docs/dataset-browse.docs.tsx @@ -3,7 +3,7 @@ import { Box } from "@mui/material"; import { markdown, ReactSpecimen } from "catalog"; import { DatasetResult } from "@/browser/dataset-browse"; -import { EditorConfiguratorStateProvider } from "@/configurator"; +import { ConfiguratorStateProvider } from "@/configurator"; import { states } from "@/docs/fixtures"; import { DataCubePublicationStatus } from "@/graphql/query-hooks"; @@ -13,7 +13,7 @@ export default () => markdown` > Dataset results are shown when selecting a dataset at the beginning of the chart creation process. ${( - markdown` /> - + )} diff --git a/app/homepage/examples.tsx b/app/homepage/examples.tsx index 8cc503e4f..015ecca44 100644 --- a/app/homepage/examples.tsx +++ b/app/homepage/examples.tsx @@ -5,7 +5,8 @@ import { ReactNode } from "react"; import { ChartPublished } from "@/components/chart-published"; import Flex from "@/components/flex"; import { HomepageSection } from "@/homepage/generic"; -import { migrateChartConfig } from "@/utils/chart-config/versioning"; +import { ConfiguratorStateProvider } from "@/src"; +import { migrateConfiguratorState } from "@/utils/chart-config/versioning"; export const Examples = ({ headline, @@ -34,11 +35,16 @@ export const Examples = ({ }} > {headline} - - - - - + + + + + - + }, + }, + })} + > + + + + {example3Headline && example3Description && ( { @@ -24,26 +32,24 @@ const Page: NextPage = () => { const importedConfig = ( await import(`../../../test/__fixtures/config/${env}/${slug}`) ).default; - setConfig(importedConfig); + setConfig({ + ...importedConfig, + data: migrateConfiguratorState(importedConfig.data), + }); }; + run(); }, [env, slug]); if (config) { - const { dataSet, dataSource, meta, chartConfig } = config.data; - const migratedConfig = migrateChartConfig(chartConfig, { - migrationProps: config.data, - }); - return ( - + + + ); } diff --git a/app/pages/_charts.tsx b/app/pages/_charts.tsx index b8f409a1e..d63574d2e 100644 --- a/app/pages/_charts.tsx +++ b/app/pages/_charts.tsx @@ -7,7 +7,11 @@ import { ChartPanelPublished } from "@/components/chart-panel"; import { ChartPublished } from "@/components/chart-published"; import Flex from "@/components/flex"; import { ContentLayout } from "@/components/layout"; -import { Config } from "@/configurator"; +import { + Config, + ConfiguratorStateProvider, + ConfiguratorStatePublished, +} from "@/configurator"; import { getAllConfigs } from "@/db/config"; type PageProps = { @@ -76,62 +80,54 @@ export const getServerSideProps = async () => { return { props: { - configs: configs.filter((c: $Unexpressable) => c.data && c.data.meta), + configs: configs.filter((c: $Unexpressable) => c.data), }, }; }; const Page: NextPage = ({ configs }) => { return ( - <> - - - - {configs.map( - ( - { key, data: { dataSet, dataSource, chartConfigs, meta } }, - i - ) => { - return chartConfigs.map((d) => ( - - - Loading...} - > - - - - Id: {key} -{" "} - - Open - {" "} - - - - )); - } - )} - - - - + + + + {configs.map((config, i) => { + return ( + + + + Loading...} + > + + + + Id: {config.key} -{" "} + + Open + {" "} + + + + + ); + })} + + + ); }; diff --git a/app/pages/browse/index.tsx b/app/pages/browse/index.tsx index b9313233f..8b8e6fb1f 100644 --- a/app/pages/browse/index.tsx +++ b/app/pages/browse/index.tsx @@ -1,6 +1,6 @@ import { SelectDatasetStep } from "@/browser/select-dataset-step"; import { AppLayout } from "@/components/layout"; -import { EditorConfiguratorStateProvider } from "@/configurator/configurator-state"; +import { ConfiguratorStateProvider } from "@/configurator/configurator-state"; import { DataCubeResultOrder } from "@/graphql/query-hooks"; export type BrowseParams = { @@ -18,12 +18,9 @@ export type BrowseParams = { export function DatasetBrowser() { return ( - + - + ); } diff --git a/app/pages/create/[chartId].tsx b/app/pages/create/[chartId].tsx index ae3a346e0..8731c9a8f 100644 --- a/app/pages/create/[chartId].tsx +++ b/app/pages/create/[chartId].tsx @@ -7,7 +7,7 @@ import { createMetadataPanelStore, MetadataPanelStoreContext, } from "@/components/metadata-panel"; -import { Configurator, EditorConfiguratorStateProvider } from "@/configurator"; +import { Configurator, ConfiguratorStateProvider } from "@/configurator"; type PageProps = { locale: string; @@ -38,11 +38,11 @@ const ChartConfiguratorPage: NextPage = ({ chartId }) => { - + - + ); diff --git a/app/pages/embed/[chartId].tsx b/app/pages/embed/[chartId].tsx index 4e0038275..edb66bc58 100644 --- a/app/pages/embed/[chartId].tsx +++ b/app/pages/embed/[chartId].tsx @@ -3,7 +3,7 @@ import { GetServerSideProps } from "next"; import ErrorPage from "next/error"; import { ChartPublished } from "@/components/chart-published"; -import { Config, getChartConfig } from "@/configurator"; +import { Config, ConfiguratorStateProvider } from "@/configurator"; import { getConfig } from "@/db/config"; import { serializeProps } from "@/db/serialize"; import { EmbedOptionsProvider } from "@/utils/embed"; @@ -26,36 +26,41 @@ export const getServerSideProps: GetServerSideProps = async ({ }) => { const config = await getConfig(query.chartId as string); - if (config && config.data) { - // TODO validate configuration - return { props: serializeProps({ status: "found", config }) }; + if (config?.data) { + return { + props: serializeProps({ + status: "found", + config, + }), + }; } res.statusCode = 404; - return { props: { status: "notfound" } }; + return { + props: { + status: "notfound", + }, + }; }; const EmbedPage = (props: PageProps) => { if (props.status === "notfound") { - // TODO: display 404 message return ; } const { config: { key, data }, } = props; - const chartConfig = getChartConfig({ ...data, state: "PUBLISHING" }); return ( - + + + ); }; diff --git a/app/pages/v/[chartId].tsx b/app/pages/v/[chartId].tsx index 989818b94..9b09bf997 100644 --- a/app/pages/v/[chartId].tsx +++ b/app/pages/v/[chartId].tsx @@ -6,14 +6,19 @@ import ErrorPage from "next/error"; import Head from "next/head"; import NextLink from "next/link"; import { useRouter } from "next/router"; -import { useEffect, useState } from "react"; +import React, { useState } from "react"; import { ChartPanelPublished } from "@/components/chart-panel"; import { ChartPublished } from "@/components/chart-published"; import { Success } from "@/components/hint"; import { ContentLayout } from "@/components/layout"; import { PublishActions } from "@/components/publish-actions"; -import { Config, getChartConfig } from "@/configurator"; +import { + Config, + ConfiguratorStateProvider, + ConfiguratorStatePublished, + getChartConfig, +} from "@/configurator"; import { getConfig } from "@/db/config"; import { Serialized, deserializeProps, serializeProps } from "@/db/serialize"; import { useLocale } from "@/locales/use-locale"; @@ -72,9 +77,26 @@ const VisualizationPage = (props: Serialized) => { const [publishSuccess] = useState(() => !!query.publishSuccess); const { status } = deserializeProps(props); - const { dataSource, setDataSource } = useDataSourceStore(); + const { key, state } = React.useMemo(() => { + if (props.status === "found") { + return { + key: props.config.key, + state: { + state: "PUBLISHED", + ...props.config.data, + } as ConfiguratorStatePublished, + }; + } - useEffect(() => { + return { + key: "", + state: undefined, + }; + }, [props]); + const chartConfig = state ? getChartConfig(state) : undefined; + + const { dataSource, setDataSource } = useDataSourceStore(); + React.useEffect(() => { // Remove publishSuccess from URL so that when reloading of sharing the link // to someone, there is no publishSuccess mention if (query.publishSuccess) { @@ -90,22 +112,24 @@ const VisualizationPage = (props: Serialized) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [dataSource.url, setDataSource, props]); - if (status === "notfound") { + if ( + status === "notfound" || + state === undefined || + chartConfig === undefined + ) { return ; } - const { key, data } = (props as Exclude) - .config; - const chartConfig = getChartConfig({ ...data, state: "PUBLISHING" }); - return ( - + {/* FIXME: possibly we'll need to copy the content of first chart when migrating / saving to db + or have additional annotator for dashboards / compositions. */} + {/* og:url is set in _app.tsx */} @@ -128,15 +152,11 @@ const VisualizationPage = (props: Serialized) => { )} - - - + + + + + {publishSuccess ? ( diff --git a/app/src/index.ts b/app/src/index.ts index 709997be4..f3e5da5c9 100644 --- a/app/src/index.ts +++ b/app/src/index.ts @@ -5,7 +5,7 @@ export { I18nProvider } from "@lingui/react"; export { Configurator, - EditorConfiguratorStateProvider, + ConfiguratorStateProvider, useConfiguratorState, } from "../configurator"; export { From 6ff1aaab8b5d13a0e152a38bd331c534390e57ea Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Wed, 6 Sep 2023 15:27:35 +0200 Subject: [PATCH 24/40] refactor: Derive editable prop dynamically --- app/components/chart-panel.tsx | 15 ++------------- app/components/chart-selection-tabs.tsx | 17 ++++++----------- app/configurator/components/configurator.tsx | 10 +++++----- app/docs/chart-selection-tabs.docs.tsx | 9 ++++++--- app/pages/_charts.tsx | 6 +++--- app/pages/v/[chartId].tsx | 6 +++--- 6 files changed, 25 insertions(+), 38 deletions(-) diff --git a/app/components/chart-panel.tsx b/app/components/chart-panel.tsx index aeef0d3f5..e279d9c43 100644 --- a/app/components/chart-panel.tsx +++ b/app/components/chart-panel.tsx @@ -5,23 +5,12 @@ import React from "react"; import { ChartSelectionTabs } from "@/components/chart-selection-tabs"; import Flex from "@/components/flex"; -export const ChartPanelConfigurator = (props: React.PropsWithChildren<{}>) => { +export const ChartPanel = (props: React.PropsWithChildren<{}>) => { const { children } = props; return ( <> - - {children} - - ); -}; - -export const ChartPanelPublished = (props: React.PropsWithChildren<{}>) => { - const { children } = props; - - return ( - <> - + {children} ); diff --git a/app/components/chart-selection-tabs.tsx b/app/components/chart-selection-tabs.tsx index cd5360e22..2dbcfce5c 100644 --- a/app/components/chart-selection-tabs.tsx +++ b/app/components/chart-selection-tabs.tsx @@ -18,6 +18,7 @@ import { ConfiguratorStatePublished, ConfiguratorStatePublishing, getChartConfig, + isPublished, useConfiguratorState, } from "@/configurator"; import { ChartTypeSelector } from "@/configurator/components/chart-type-selector"; @@ -62,12 +63,7 @@ const TabsStateProvider = ({ children }: { children: ReactNode }) => { ); }; -export const ChartSelectionTabs = ({ - editable, -}: { - /** Tabs are not editable when they are published. */ - editable: boolean; -}) => { +export const ChartSelectionTabs = () => { const [state] = useConfiguratorState() as [ ( | ConfiguratorStateConfiguringChart @@ -76,6 +72,8 @@ export const ChartSelectionTabs = ({ ), Dispatch ]; + const editable = + state.state === "CONFIGURING_CHART" || state.state === "PUBLISHING"; if (!editable && state.chartConfigs.length === 1) { return null; @@ -121,10 +119,7 @@ const useStyles = makeStyles((theme) => ({ })); type TabsEditableProps = { - state: - | ConfiguratorStateConfiguringChart - | ConfiguratorStatePublishing - | ConfiguratorStatePublished; + state: ConfiguratorStateConfiguringChart | ConfiguratorStatePublishing; chartConfig: ChartConfig; data: TabDatum[]; }; @@ -215,7 +210,7 @@ type TabsFixedProps = { const TabsFixed = (props: TabsFixedProps) => { const { data } = props; - const [, dispatch] = useConfiguratorState(); + const [, dispatch] = useConfiguratorState(isPublished); return ( { )} - + - + { return ( - + - + ); }; diff --git a/app/docs/chart-selection-tabs.docs.tsx b/app/docs/chart-selection-tabs.docs.tsx index 97abe271a..b433fec41 100644 --- a/app/docs/chart-selection-tabs.docs.tsx +++ b/app/docs/chart-selection-tabs.docs.tsx @@ -24,7 +24,7 @@ ${( } allowDefaultRedirect={false} > - + )} @@ -36,11 +36,14 @@ ${( - + )} diff --git a/app/pages/_charts.tsx b/app/pages/_charts.tsx index d63574d2e..464d85a82 100644 --- a/app/pages/_charts.tsx +++ b/app/pages/_charts.tsx @@ -3,7 +3,7 @@ import { NextPage } from "next"; import NextLink from "next/link"; import React, { useEffect, useRef, useState } from "react"; -import { ChartPanelPublished } from "@/components/chart-panel"; +import { ChartPanel } from "@/components/chart-panel"; import { ChartPublished } from "@/components/chart-published"; import Flex from "@/components/flex"; import { ContentLayout } from "@/components/layout"; @@ -101,7 +101,7 @@ const Page: NextPage = ({ configs }) => { chartId="published" initialState={config.data as ConfiguratorStatePublished} > - + Loading...} @@ -120,7 +120,7 @@ const Page: NextPage = ({ configs }) => { Open {" "} - + ); diff --git a/app/pages/v/[chartId].tsx b/app/pages/v/[chartId].tsx index 9b09bf997..f8af95532 100644 --- a/app/pages/v/[chartId].tsx +++ b/app/pages/v/[chartId].tsx @@ -8,7 +8,7 @@ import NextLink from "next/link"; import { useRouter } from "next/router"; import React, { useState } from "react"; -import { ChartPanelPublished } from "@/components/chart-panel"; +import { ChartPanel } from "@/components/chart-panel"; import { ChartPublished } from "@/components/chart-published"; import { Success } from "@/components/hint"; import { ContentLayout } from "@/components/layout"; @@ -153,9 +153,9 @@ const VisualizationPage = (props: Serialized) => { )} - + - + From 9380e6de14aed5a7a7333425154a999829f9aff6 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Wed, 6 Sep 2023 15:46:42 +0200 Subject: [PATCH 25/40] style: Improve design of SelectionTabs --- app/components/chart-selection-tabs.tsx | 30 ++++++++++++------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/app/components/chart-selection-tabs.tsx b/app/components/chart-selection-tabs.tsx index 2dbcfce5c..c5dd78a1e 100644 --- a/app/components/chart-selection-tabs.tsx +++ b/app/components/chart-selection-tabs.tsx @@ -99,7 +99,7 @@ export const ChartSelectionTabs = () => { ); }; -const useStyles = makeStyles((theme) => ({ +const useStyles = makeStyles((theme) => ({ editableChartTypeSelector: { width: 320, padding: `0 ${theme.spacing(3)} ${theme.spacing(3)}`, @@ -108,11 +108,13 @@ const useStyles = makeStyles((theme) => ({ gap: theme.spacing(1), alignItems: "center", padding: theme.spacing(2), - borderRadius: 3, + borderTopLeftRadius: theme.spacing(1), + borderTopRightRadius: theme.spacing(1), transition: "0.125s ease background-color", cursor: "default", }, tabContentIconContainer: { + padding: 0, minWidth: "fit-content", color: theme.palette.grey[700], }, @@ -300,12 +302,12 @@ const TabsInner = ({ `1px solid ${theme.palette.divider}`, + borderBottom: (theme) => + `1px solid ${d.active ? "transparent" : theme.palette.divider}`, minWidth: "fit-content", }} label={ @@ -326,11 +328,12 @@ const TabsInner = ({ {editable && ( `-${theme.spacing(2)}`, p: 0, background: "white", - border: "1px solid", - borderBottomWidth: 0, - borderColor: "divider", + borderBottomWidth: 1, + borderBottomStyle: "solid", + borderBottomColor: "divider", minWidth: "fit-content", }} onClick={onAddButtonClick} @@ -368,7 +371,7 @@ const TabContent = ({ ) => void; onSwitchClick?: (e: React.MouseEvent) => void; }) => { - const classes = useStyles({ editable }); + const classes = useStyles(); return ( @@ -377,12 +380,7 @@ const TabContent = ({ onClick={onSwitchClick} sx={{ minWidth: "fit-content", - backgroundColor: active ? "primary.main" : undefined, - color: active ? "primary.contrastText" : undefined, - - "&:hover": { - backgroundColor: active ? "primary.main" : undefined, - }, + color: active ? "primary.main" : "secondary.active", }} > From 4fd1becf6184805987b78d99808c7284850175f2 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Thu, 7 Sep 2023 09:56:16 +0200 Subject: [PATCH 26/40] feat: Add a way to add a given chart type --- app/components/chart-selection-tabs.tsx | 68 +++++++++++++------ .../components/chart-type-selector.tsx | 40 ++++++----- app/configurator/config-form.tsx | 38 ++++++++--- app/configurator/configurator-state.tsx | 14 +++- 4 files changed, 109 insertions(+), 51 deletions(-) diff --git a/app/components/chart-selection-tabs.tsx b/app/components/chart-selection-tabs.tsx index c5dd78a1e..e44055712 100644 --- a/app/components/chart-selection-tabs.tsx +++ b/app/components/chart-selection-tabs.tsx @@ -6,7 +6,6 @@ import React, { ReactNode, createContext, useContext, - useEffect, useState, } from "react"; @@ -29,11 +28,11 @@ import { } from "@/graphql/query-hooks"; import { Icon, IconName } from "@/icons"; import { useLocale } from "@/src"; -import { createChartId } from "@/utils/create-chart-id"; import useEvent from "@/utils/use-event"; type TabsState = { isPopoverOpen: boolean; + popoverType: "edit" | "add"; activeChartKey?: string; }; @@ -54,7 +53,10 @@ export const useTabsState = () => { }; const TabsStateProvider = ({ children }: { children: ReactNode }) => { - const [state, dispatch] = useState({ isPopoverOpen: false }); + const [state, dispatch] = useState({ + popoverType: "add", + isPopoverOpen: false, + }); return ( @@ -85,6 +87,7 @@ export const ChartSelectionTabs = () => { key: d.key, chartType: d.chartType, active: d.key === chartConfig.key, + editable: editable && state.chartConfigs.length > 1, }; }); @@ -140,15 +143,24 @@ const TabsEditable = (props: TabsEditableProps) => { setPopoverAnchorEl(null); setTabsState({ isPopoverOpen: false, + popoverType: tabsState.popoverType, activeChartKey: tabsState.activeChartKey, }); }); - useEffect(() => { + React.useEffect(() => { handleClose(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [chartConfig.chartType]); + React.useEffect(() => { + setTabsState({ + isPopoverOpen: false, + popoverType: "add", + activeChartKey: chartConfig.key, + }); + }, [chartConfig.key, setTabsState]); + return ( <> { ) => { e.stopPropagation(); setPopoverAnchorEl(e.currentTarget); - setTabsState({ isPopoverOpen: true, activeChartKey }); + setTabsState({ + isPopoverOpen: true, + popoverType: "edit", + activeChartKey, + }); }} onSwitchButtonClick={(key: string) => { dispatch({ @@ -168,15 +184,13 @@ const TabsEditable = (props: TabsEditableProps) => { value: key, }); }} - onAddButtonClick={() => { - dispatch({ - type: "CHART_CONFIG_ADD", - value: { - chartConfig: { - ...chartConfig, - key: createChartId(), - }, - }, + onAddButtonClick={(e) => { + e.stopPropagation(); + setPopoverAnchorEl(e.currentTarget); + setTabsState({ + isPopoverOpen: true, + popoverType: "add", + activeChartKey: tabsState.activeChartKey, }); }} /> @@ -190,11 +204,22 @@ const TabsEditable = (props: TabsEditableProps) => { }} onClose={handleClose} > - + {tabsState.popoverType === "add" ? ( + + ) : ( + + + + )} ); @@ -204,6 +229,7 @@ type TabDatum = { key: string; chartType: ChartType; active: boolean; + editable: boolean; }; type TabsFixedProps = { @@ -289,7 +315,7 @@ const TabsInner = ({ activeChartKey: string ) => void; onSwitchButtonClick?: (key: string) => void; - onAddButtonClick?: () => void; + onAddButtonClick?: (e: React.MouseEvent) => void; }) => { return ( @@ -314,7 +340,7 @@ const TabsInner = ({ { diff --git a/app/configurator/components/chart-type-selector.tsx b/app/configurator/components/chart-type-selector.tsx index ad41621bb..02c123bd1 100644 --- a/app/configurator/components/chart-type-selector.tsx +++ b/app/configurator/components/chart-type-selector.tsx @@ -11,7 +11,7 @@ import { ControlSectionSkeleton } from "@/configurator/components/chart-controls import { getFieldLabel } from "@/configurator/components/field-i18n"; import { getIconName } from "@/configurator/components/ui-helpers"; import { FieldProps, useChartType } from "@/configurator/config-form"; -import { useComponentsQuery } from "@/graphql/query-hooks"; +import { useComponentsWithHierarchiesQuery } from "@/graphql/query-hooks"; import { Icon } from "@/icons"; import { useLocale } from "@/locales/use-locale"; @@ -99,20 +99,20 @@ export const ChartTypeSelectionButton = ({ export const ChartTypeSelector = ({ state, + type = "edit", showHelp, chartKey, sx, ...props }: { state: ConfiguratorStateConfiguringChart | ConfiguratorStatePublishing; + type?: "add" | "edit"; showHelp?: boolean; chartKey: string; sx?: BoxProps["sx"]; } & BoxProps) => { const locale = useLocale(); - const { value: chartType, onChange: onChangeChartType } = - useChartType(chartKey); - const [{ data }] = useComponentsQuery({ + const [{ data }] = useComponentsWithHierarchiesQuery({ variables: { iri: state.dataSet, sourceType: state.dataSource.type, @@ -120,6 +120,12 @@ export const ChartTypeSelector = ({ locale, }, }); + const { value: chartType, onChange: onChangeChartType } = useChartType( + chartKey, + type, + data?.dataCubeByIri?.dimensions ?? [], + data?.dataCubeByIri?.measures ?? [] + ); if (!data?.dataCubeByIri) { return ; @@ -135,17 +141,21 @@ export const ChartTypeSelector = ({ Chart Type - {showHelp !== false ? ( + {showHelp === false ? null : ( - - Switch to another chart type while maintaining most filter - settings. - + {type === "add" ? ( + + Add another chart type. + + ) : ( + + Switch to another chart type while maintaining most filter + settings. + + )} - ) : ( - false )}
@@ -171,7 +181,7 @@ export const ChartTypeSelector = ({ key={d} label={d} value={d} - checked={chartType === d} + checked={type === "edit" ? chartType === d : false} disabled={!possibleChartTypes.includes(d)} onClick={(e) => onChangeChartType(e.currentTarget.value as ChartType) @@ -179,12 +189,6 @@ export const ChartTypeSelector = ({ /> ))} - {/* TODO: Handle properly when chart composition is implemented */} - {/* */} )}
diff --git a/app/configurator/config-form.tsx b/app/configurator/config-form.tsx index 8a10a3df7..4ee41dda2 100644 --- a/app/configurator/config-form.tsx +++ b/app/configurator/config-form.tsx @@ -12,7 +12,7 @@ import React, { } from "react"; import { useClient } from "urql"; -import { getFieldComponentIri } from "@/charts"; +import { getFieldComponentIri, getInitialConfig } from "@/charts"; import { EncodingFieldType } from "@/charts/chart-config-ui-options"; import { ChartConfig, ChartType, getChartConfig } from "@/config-types"; import { @@ -29,6 +29,7 @@ import { DimensionValuesQuery, } from "@/graphql/query-hooks"; import { HierarchyValue } from "@/graphql/resolver-types"; +import { DataCubeMetadataWithHierarchies } from "@/graphql/types"; import { useLocale } from "@/locales/use-locale"; import { bfs } from "@/utils/bfs"; import { isMultiHierarchyNode } from "@/utils/hierarchy"; @@ -395,7 +396,10 @@ export const useActiveFieldField = ({ // Specific ------------------------------------------------------------------ export const useChartType = ( - chartKey: string + chartKey: string, + type: "add" | "edit" = "edit", + dimensions: DataCubeMetadataWithHierarchies["dimensions"], + measures: DataCubeMetadataWithHierarchies["measures"] ): { value: ChartType; onChange: (chartType: ChartType) => void; @@ -404,14 +408,28 @@ export const useChartType = ( const [state, dispatch] = useConfiguratorState(); const chartConfig = getChartConfig(state, chartKey); const onChange = useEvent((chartType: ChartType) => { - dispatch({ - type: "CHART_TYPE_CHANGED", - value: { - locale, - chartKey, - chartType, - }, - }); + if (type === "edit") { + dispatch({ + type: "CHART_TYPE_CHANGED", + value: { + locale, + chartKey, + chartType, + }, + }); + } else { + dispatch({ + type: "CHART_CONFIG_ADD", + value: { + chartConfig: getInitialConfig({ + chartType, + dimensions, + measures, + }), + locale, + }, + }); + } }); const value = get(chartConfig, "chartType"); diff --git a/app/configurator/configurator-state.tsx b/app/configurator/configurator-state.tsx index 04d996c82..457ff6dd0 100644 --- a/app/configurator/configurator-state.tsx +++ b/app/configurator/configurator-state.tsx @@ -295,6 +295,7 @@ export type ConfiguratorStateAction = type: "CHART_CONFIG_ADD"; value: { chartConfig: ChartConfig; + locale: Locale; }; } | { @@ -1243,8 +1244,17 @@ const reducer: Reducer = ( case "CHART_CONFIG_ADD": if (draft.state === "CONFIGURING_CHART") { - draft.chartConfigs.push(action.value.chartConfig); - draft.activeChartKey = action.value.chartConfig.key; + const metadata = getCachedMetadata(draft, action.value.locale); + + if (metadata) { + draft.chartConfigs.push( + deriveFiltersFromFields( + action.value.chartConfig, + metadata.dimensions + ) + ); + draft.activeChartKey = action.value.chartConfig.key; + } } return draft; From 080925e5ad67f42ddd0737ea7dba25fa53929214 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Thu, 7 Sep 2023 13:14:15 +0200 Subject: [PATCH 27/40] feat: Add a way to remove charts --- app/components/chart-selection-tabs.tsx | 13 +++++++++++- .../components/chart-type-selector.tsx | 4 +--- app/configurator/configurator-state.tsx | 21 +++++++++++++++++++ app/locales/de/messages.po | 11 ++++++++++ app/locales/en/messages.po | 11 ++++++++++ app/locales/fr/messages.po | 13 +++++++++++- app/locales/it/messages.po | 11 ++++++++++ 7 files changed, 79 insertions(+), 5 deletions(-) diff --git a/app/components/chart-selection-tabs.tsx b/app/components/chart-selection-tabs.tsx index e44055712..6de7105bc 100644 --- a/app/components/chart-selection-tabs.tsx +++ b/app/components/chart-selection-tabs.tsx @@ -213,7 +213,18 @@ const TabsEditable = (props: TabsEditableProps) => { /> ) : ( - + )} @@ -240,7 +264,6 @@ type TabDatum = { key: string; chartType: ChartType; active: boolean; - editable: boolean; }; type TabsFixedProps = { @@ -352,7 +375,7 @@ const TabsInner = ({ { diff --git a/app/locales/de/messages.po b/app/locales/de/messages.po index 2be5919ef..c94aa25fe 100644 --- a/app/locales/de/messages.po +++ b/app/locales/de/messages.po @@ -338,6 +338,10 @@ msgstr "Beschreibung hinzufügen" msgid "controls.dimensionvalue.none" msgstr "Kein Filter" +#: app/components/chart-selection-tabs.tsx +msgid "controls.duplicate.visualization" +msgstr "Duplizieren Sie diese Visualisierung" + #: app/configurator/components/filters.tsx msgid "controls.filter.nb-elements" msgstr "{0} von {1}" diff --git a/app/locales/en/messages.po b/app/locales/en/messages.po index a209aa18c..b1b47523d 100644 --- a/app/locales/en/messages.po +++ b/app/locales/en/messages.po @@ -338,6 +338,10 @@ msgstr "Description" msgid "controls.dimensionvalue.none" msgstr "No Filter" +#: app/components/chart-selection-tabs.tsx +msgid "controls.duplicate.visualization" +msgstr "Duplicate this visualization" + #: app/configurator/components/filters.tsx msgid "controls.filter.nb-elements" msgstr "{0} of {1}" diff --git a/app/locales/fr/messages.po b/app/locales/fr/messages.po index e40f600bb..0f4c4a2bb 100644 --- a/app/locales/fr/messages.po +++ b/app/locales/fr/messages.po @@ -338,6 +338,10 @@ msgstr "Description" msgid "controls.dimensionvalue.none" msgstr "Aucune filtre" +#: app/components/chart-selection-tabs.tsx +msgid "controls.duplicate.visualization" +msgstr "Dupliquer cette visualisation" + #: app/configurator/components/filters.tsx msgid "controls.filter.nb-elements" msgstr "{0} sur {1}" diff --git a/app/locales/it/messages.po b/app/locales/it/messages.po index 4223570c3..42b6ce7f8 100644 --- a/app/locales/it/messages.po +++ b/app/locales/it/messages.po @@ -338,6 +338,10 @@ msgstr "Descrizione" msgid "controls.dimensionvalue.none" msgstr "Nessun filtro" +#: app/components/chart-selection-tabs.tsx +msgid "controls.duplicate.visualization" +msgstr "Duplicare questa visualizzazione" + #: app/configurator/components/filters.tsx msgid "controls.filter.nb-elements" msgstr "{0} di {1}" From 8165d6570a3929fc4ec746918cec65a43ecb65f2 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Thu, 7 Sep 2023 14:21:58 +0200 Subject: [PATCH 30/40] feat: Reverse remove and duplicate this visualization buttons --- app/components/chart-selection-tabs.tsx | 38 ++++++++++++------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/app/components/chart-selection-tabs.tsx b/app/components/chart-selection-tabs.tsx index cb5463345..454ace6ff 100644 --- a/app/components/chart-selection-tabs.tsx +++ b/app/components/chart-selection-tabs.tsx @@ -214,25 +214,6 @@ const TabsEditable = (props: TabsEditableProps) => { /> ) : ( - {state.chartConfigs.length > 1 && ( - - )} + {state.chartConfigs.length > 1 && ( + + )} )} From 56637755857dcc6d069236ce86c59bc5699f7a6f Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Thu, 7 Sep 2023 15:48:43 +0200 Subject: [PATCH 31/40] feat: Hide dashboards behind a flag --- app/components/chart-selection-tabs.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/components/chart-selection-tabs.tsx b/app/components/chart-selection-tabs.tsx index 454ace6ff..8d90f27f8 100644 --- a/app/components/chart-selection-tabs.tsx +++ b/app/components/chart-selection-tabs.tsx @@ -22,6 +22,7 @@ import { } from "@/configurator"; import { ChartTypeSelector } from "@/configurator/components/chart-type-selector"; import { getIconName } from "@/configurator/components/ui-helpers"; +import { flag } from "@/flags"; import { useComponentsQuery, useDataCubeMetadataQuery, @@ -375,7 +376,7 @@ const TabsInner = ({ { @@ -386,7 +387,7 @@ const TabsInner = ({ } /> ))} - {editable && ( + {editable && flag("dashboards") && ( `-${theme.spacing(2)}`, From 71430dd244d2d335d8c28c71797578917ad98e8a Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Fri, 8 Sep 2023 13:58:48 +0200 Subject: [PATCH 32/40] fix: Reset chart container height on chart switch --- app/components/chart-table-preview.tsx | 37 ++++++++++++------------- app/configurator/configurator-state.tsx | 16 +++++++++++ 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/app/components/chart-table-preview.tsx b/app/components/chart-table-preview.tsx index d6a338df5..4c5ba15d9 100644 --- a/app/components/chart-table-preview.tsx +++ b/app/components/chart-table-preview.tsx @@ -1,15 +1,6 @@ -import { - createContext, - Dispatch, - ReactNode, - SetStateAction, - useContext, - useState, - useCallback, - useRef, - useMemo, - RefObject, -} from "react"; +import React, { Dispatch, ReactNode, RefObject, SetStateAction } from "react"; + +import { hasChartConfigs, useConfiguratorState } from "@/configurator"; type Context = { state: boolean; @@ -20,7 +11,7 @@ type Context = { computeContainerHeight: () => void; }; -const ChartTablePreviewContext = createContext({ +const ChartTablePreviewContext = React.createContext({ state: true, setState: () => {}, setStateRaw: () => {}, @@ -30,7 +21,7 @@ const ChartTablePreviewContext = createContext({ }); export const useChartTablePreview = () => { - const ctx = useContext(ChartTablePreviewContext); + const ctx = React.useContext(ChartTablePreviewContext); if (ctx === undefined) { throw Error( @@ -51,25 +42,32 @@ export const ChartTablePreviewProvider = ({ }: { children: ReactNode; }) => { - const [state, setStateRaw] = useState(false); - const containerHeight = useRef("auto" as "auto" | number); - const containerRef = useRef(null); + const [configuratorState] = useConfiguratorState(hasChartConfigs); + const [state, setStateRaw] = React.useState(false); + const containerHeight = React.useRef("auto" as "auto" | number); + const containerRef = React.useRef(null); const computeContainerHeight = () => { if (!containerRef.current) { return; } + const bcr = containerRef.current.getBoundingClientRect(); containerHeight.current = bcr.height; }; - const setState = useCallback( + const setState = React.useCallback( (v) => { computeContainerHeight(); + return setStateRaw(v); }, [setStateRaw] ); - const ctx = useMemo(() => { + React.useEffect(() => { + containerHeight.current = "auto"; + }, [configuratorState.activeChartKey]); + + const ctx = React.useMemo(() => { return { state, setState, @@ -79,6 +77,7 @@ export const ChartTablePreviewProvider = ({ computeContainerHeight, }; }, [setState, state, containerRef, containerHeight, setStateRaw]); + return ( {children} diff --git a/app/configurator/configurator-state.tsx b/app/configurator/configurator-state.tsx index d737fa1cf..1a1c672d3 100644 --- a/app/configurator/configurator-state.tsx +++ b/app/configurator/configurator-state.tsx @@ -37,6 +37,7 @@ import { ConfiguratorState, ConfiguratorStateConfiguringChart, ConfiguratorStatePublished, + ConfiguratorStatePublishing, ConfiguratorStateSelectingDataSet, DataSource, FilterValue, @@ -1601,8 +1602,23 @@ export const isConfiguring = ( return s.state === "CONFIGURING_CHART"; }; +export const isPublishing = ( + s: ConfiguratorState +): s is ConfiguratorStatePublishing => { + return s.state === "PUBLISHING"; +}; + export const isPublished = ( s: ConfiguratorState ): s is ConfiguratorStatePublished => { return s.state === "PUBLISHED"; }; + +export const hasChartConfigs = ( + s: ConfiguratorState +): s is + | ConfiguratorStateConfiguringChart + | ConfiguratorStatePublishing + | ConfiguratorStatePublished => { + return isConfiguring(s) || isPublishing(s) || isPublished(s); +}; From 96a59f141a069b4640c3f84c7b52dc00e0cd8d22 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Fri, 8 Sep 2023 13:58:56 +0200 Subject: [PATCH 33/40] refactor: Use type guard --- app/components/chart-selection-tabs.tsx | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/app/components/chart-selection-tabs.tsx b/app/components/chart-selection-tabs.tsx index 8d90f27f8..4cc62b5ac 100644 --- a/app/components/chart-selection-tabs.tsx +++ b/app/components/chart-selection-tabs.tsx @@ -14,9 +14,9 @@ import { ChartConfig, ChartType, ConfiguratorStateConfiguringChart, - ConfiguratorStatePublished, ConfiguratorStatePublishing, getChartConfig, + hasChartConfigs, isPublished, useConfiguratorState, } from "@/configurator"; @@ -68,14 +68,7 @@ const TabsStateProvider = ({ children }: { children: ReactNode }) => { }; export const ChartSelectionTabs = () => { - const [state] = useConfiguratorState() as [ - ( - | ConfiguratorStateConfiguringChart - | ConfiguratorStatePublishing - | ConfiguratorStatePublished - ), - Dispatch - ]; + const [state] = useConfiguratorState(hasChartConfigs); const editable = state.state === "CONFIGURING_CHART" || state.state === "PUBLISHING"; From eb1e3150e9ba8064599b0f8a9fe84cb83f874ee8 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Mon, 11 Sep 2023 16:29:09 +0200 Subject: [PATCH 34/40] docs: Add comment --- app/configurator/configurator-state.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/configurator/configurator-state.tsx b/app/configurator/configurator-state.tsx index 1a1c672d3..9ac912683 100644 --- a/app/configurator/configurator-state.tsx +++ b/app/configurator/configurator-state.tsx @@ -1491,6 +1491,13 @@ const ConfiguratorStateProviderInternal = ({ }; }), ], + // Technically, we do not need to store the active chart key, as + // it's only used in the edit mode, but it makes it easier to manage + // the state when retrieving the chart from the database. Potentially, + // it might also be useful for other things in the future (e.g. when we + // have multiple charts in the "stepper mode", and we'd like to start + // the story from a specific point and e.g. toggle back and forth between + // the different charts). activeChartKey: state.chartConfigs[0].key, }); From f8298f2f2a325119f5fb1d1e6c7d57d2e44fd0be Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Tue, 12 Sep 2023 14:23:36 +0200 Subject: [PATCH 35/40] refactor: Clean up --- app/components/chart-selection-tabs.tsx | 169 ++++++++++-------------- 1 file changed, 71 insertions(+), 98 deletions(-) diff --git a/app/components/chart-selection-tabs.tsx b/app/components/chart-selection-tabs.tsx index 4cc62b5ac..43ac79ef5 100644 --- a/app/components/chart-selection-tabs.tsx +++ b/app/components/chart-selection-tabs.tsx @@ -1,13 +1,7 @@ import { Trans } from "@lingui/macro"; import { Box, Button, Popover, Tab, Tabs, Theme } from "@mui/material"; import { makeStyles } from "@mui/styles"; -import React, { - Dispatch, - ReactNode, - createContext, - useContext, - useState, -} from "react"; +import React from "react"; import Flex from "@/components/flex"; import { @@ -33,17 +27,23 @@ import { createChartId } from "@/utils/create-chart-id"; import useEvent from "@/utils/use-event"; type TabsState = { - isPopoverOpen: boolean; + popoverOpen: boolean; popoverType: "edit" | "add"; - activeChartKey?: string; + activeChartKey: string; }; -const TabsStateContext = createContext< - [TabsState, Dispatch] | undefined +const TABS_STATE: TabsState = { + popoverOpen: false, + popoverType: "add", + activeChartKey: "", +}; + +const TabsStateContext = React.createContext< + [TabsState, React.Dispatch] | undefined >(undefined); export const useTabsState = () => { - const ctx = useContext(TabsStateContext); + const ctx = React.useContext(TabsStateContext); if (ctx === undefined) { throw Error( @@ -54,11 +54,9 @@ export const useTabsState = () => { return ctx; }; -const TabsStateProvider = ({ children }: { children: ReactNode }) => { - const [state, dispatch] = useState({ - popoverType: "add", - isPopoverOpen: false, - }); +const TabsStateProvider = (props: React.PropsWithChildren<{}>) => { + const { children } = props; + const [state, dispatch] = React.useState(TABS_STATE); return ( @@ -128,16 +126,14 @@ const TabsEditable = (props: TabsEditableProps) => { const [, dispatch] = useConfiguratorState(); const locale = useLocale(); const [tabsState, setTabsState] = useTabsState(); - const [popoverAnchorEl, setPopoverAnchorEl] = useState( - null - ); - + const [popoverAnchorEl, setPopoverAnchorEl] = + React.useState(null); const classes = useStyles({ editable: true }); const handleClose = useEvent(() => { setPopoverAnchorEl(null); setTabsState({ - isPopoverOpen: false, + popoverOpen: false, popoverType: tabsState.popoverType, activeChartKey: tabsState.activeChartKey, }); @@ -146,52 +142,40 @@ const TabsEditable = (props: TabsEditableProps) => { React.useEffect(() => { handleClose(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [chartConfig.chartType]); - - React.useEffect(() => { - setTabsState({ - isPopoverOpen: false, - popoverType: "add", - activeChartKey: chartConfig.key, - }); - }, [chartConfig.key, setTabsState]); + }, [state.activeChartKey]); return ( <> , - activeChartKey: string - ) => { - e.stopPropagation(); + onChartAdd={(e) => { setPopoverAnchorEl(e.currentTarget); setTabsState({ - isPopoverOpen: true, + popoverOpen: true, + popoverType: "add", + activeChartKey: tabsState.activeChartKey, + }); + }} + onChartEdit={(e, key) => { + setPopoverAnchorEl(e.currentTarget); + setTabsState({ + popoverOpen: true, popoverType: "edit", - activeChartKey, + activeChartKey: key, }); }} - onSwitchButtonClick={(key: string) => { + onChartSwitch={(key) => { dispatch({ type: "SWITCH_ACTIVE_CHART", value: key, }); }} - onAddButtonClick={(e) => { - e.stopPropagation(); - setPopoverAnchorEl(e.currentTarget); - setTabsState({ - isPopoverOpen: true, - popoverType: "add", - activeChartKey: tabsState.activeChartKey, - }); - }} /> + { Duplicate this visualization + {state.chartConfigs.length > 1 && ( + {editable && ( - + <> + + + + + )}
); diff --git a/app/configurator/components/chart-controls/drag-and-drop-tab.tsx b/app/configurator/components/chart-controls/drag-and-drop-tab.tsx index e055101b9..4a31876a2 100644 --- a/app/configurator/components/chart-controls/drag-and-drop-tab.tsx +++ b/app/configurator/components/chart-controls/drag-and-drop-tab.tsx @@ -82,14 +82,9 @@ export const TabDropZone = ({ return ( { const locale = useLocale(); const [{ data }] = useComponentsWithHierarchiesQuery({ @@ -137,7 +135,7 @@ export const ChartTypeSelector = ({ }); return ( - + Chart Type diff --git a/app/configurator/configurator-state.tsx b/app/configurator/configurator-state.tsx index 9ac912683..07d0f38d9 100644 --- a/app/configurator/configurator-state.tsx +++ b/app/configurator/configurator-state.tsx @@ -305,6 +305,13 @@ export type ConfiguratorStateAction = chartKey: string; }; } + | { + type: "CHART_CONFIG_REORDER"; + value: { + oldIndex: number; + newIndex: number; + }; + } | { type: "SWITCH_ACTIVE_CHART"; value: string; @@ -1277,6 +1284,15 @@ const reducer: Reducer = ( return draft; + case "CHART_CONFIG_REORDER": + if (draft.state === "CONFIGURING_CHART") { + const { oldIndex, newIndex } = action.value; + const [removed] = draft.chartConfigs.splice(oldIndex, 1); + draft.chartConfigs.splice(newIndex, 0, removed); + } + + return draft; + case "SWITCH_ACTIVE_CHART": if (draft.state === "CONFIGURING_CHART" || draft.state === "PUBLISHED") { draft.activeChartKey = action.value; From 7fc6d4e5022a31feb9763a40b935c5fe418b9cba Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Fri, 15 Sep 2023 10:52:10 +0200 Subject: [PATCH 37/40] feat: Only make it possible to drag chart tab when it makes sense --- app/components/chart-selection-tabs.tsx | 48 ++++++++++++++----------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/app/components/chart-selection-tabs.tsx b/app/components/chart-selection-tabs.tsx index c9ce89698..555ef72a9 100644 --- a/app/components/chart-selection-tabs.tsx +++ b/app/components/chart-selection-tabs.tsx @@ -128,6 +128,7 @@ const TabsEditable = (props: TabsEditableProps) => { 1} onChartAdd={(e) => { setPopoverAnchorEl(e.currentTarget); setTabsState({ @@ -236,6 +237,7 @@ const TabsFixed = (props: TabsFixedProps) => { { dispatch({ type: "SWITCH_ACTIVE_CHART", @@ -290,13 +292,15 @@ const PublishChartButton = () => { type TabsInnerProps = { data: TabDatum[]; editable: boolean; + draggable: boolean; onChartAdd?: (e: React.MouseEvent) => void; onChartEdit?: (e: React.MouseEvent, key: string) => void; onChartSwitch?: (key: string) => void; }; const TabsInner = (props: TabsInnerProps) => { - const { data, editable, onChartEdit, onChartAdd, onChartSwitch } = props; + const { data, editable, draggable, onChartEdit, onChartAdd, onChartSwitch } = + props; const [, dispatch] = useConfiguratorState(); return ( @@ -362,6 +366,7 @@ const TabsInner = (props: TabsInnerProps) => { iconName={getIconName(d.chartType)} chartKey={d.key} editable={editable && flag("dashboards")} + draggable={draggable && flag("dashboards")} active={d.active} dragging={snapshot.isDragging} onEditClick={(e) => { @@ -447,6 +452,7 @@ type TabContentProps = { iconName: IconName; chartKey: string; editable?: boolean; + draggable?: boolean; active?: boolean; dragging?: boolean; onEditClick?: ( @@ -461,6 +467,7 @@ const TabContent = (props: TabContentProps) => { iconName, chartKey, editable, + draggable, active, dragging, onEditClick, @@ -479,25 +486,26 @@ const TabContent = (props: TabContentProps) => { {editable && ( - <> - - - - - + + )} + + {draggable && ( + + + )} ); From 8caacc73b40228c9414bbff8fee605679635cbca Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Fri, 15 Sep 2023 11:12:33 +0200 Subject: [PATCH 38/40] feat: Add initial scroll behavior to chart tabs --- app/components/chart-selection-tabs.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/app/components/chart-selection-tabs.tsx b/app/components/chart-selection-tabs.tsx index 555ef72a9..91bb64126 100644 --- a/app/components/chart-selection-tabs.tsx +++ b/app/components/chart-selection-tabs.tsx @@ -283,6 +283,7 @@ const PublishChartButton = () => { color="primary" variant="contained" onClick={metadata && components ? goNext : undefined} + sx={{ minWidth: "fit-content" }} > Publish this visualization @@ -304,7 +305,15 @@ const TabsInner = (props: TabsInnerProps) => { const [, dispatch] = useConfiguratorState(); return ( - + { if (d.destination && d.source.index !== d.destination.index) { @@ -326,9 +335,11 @@ const TabsInner = (props: TabsInnerProps) => { {(provided) => ( {data.map((d, i) => ( From f74724521b40ae97d5fa466b4a6d06a4f4635e45 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Fri, 15 Sep 2023 11:22:08 +0200 Subject: [PATCH 39/40] refactor: Smaller improvements --- app/components/chart-selection-tabs.tsx | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/app/components/chart-selection-tabs.tsx b/app/components/chart-selection-tabs.tsx index 91bb64126..83ec3650f 100644 --- a/app/components/chart-selection-tabs.tsx +++ b/app/components/chart-selection-tabs.tsx @@ -306,9 +306,8 @@ const TabsInner = (props: TabsInnerProps) => { return ( { } }} > - + {(provided) => ( { {(provided, snapshot) => { const { style } = provided.draggableProps; + // Limit the drag movement to the x-axis. const transform = style?.transform ? `${style.transform.split(",")[0]}, 0px)` : undefined; @@ -354,16 +350,12 @@ const TabsInner = (props: TabsInnerProps) => { ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps} - style={{ - ...provided.draggableProps.style, - transform, - opacity: 1, - }} + style={{ ...style, transform, opacity: 1 }} key={d.key} sx={{ mr: 2, p: 0, - background: "white", + background: (theme) => theme.palette.background.paper, border: (theme) => `1px solid ${theme.palette.divider}`, borderBottom: (theme) => From 5afbba34ef74bd3e33c5df7a1160ebd34c23044f Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Tue, 10 Oct 2023 11:16:28 +0200 Subject: [PATCH 40/40] chore: Remove dashboards flag --- app/components/chart-selection-tabs.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/components/chart-selection-tabs.tsx b/app/components/chart-selection-tabs.tsx index 83ec3650f..984bea70e 100644 --- a/app/components/chart-selection-tabs.tsx +++ b/app/components/chart-selection-tabs.tsx @@ -17,7 +17,6 @@ import { } from "@/configurator"; import { ChartTypeSelector } from "@/configurator/components/chart-type-selector"; import { getIconName } from "@/configurator/components/ui-helpers"; -import { flag } from "@/flags"; import { useComponentsQuery, useDataCubeMetadataQuery, @@ -368,8 +367,8 @@ const TabsInner = (props: TabsInnerProps) => { { @@ -387,7 +386,7 @@ const TabsInner = (props: TabsInnerProps) => { ))}
{provided.placeholder}
- {editable && flag("dashboards") && ( + {editable && ( `-${theme.spacing(2)}`,