From 9465641a6bd834db10bb9e6e63c1861a24fe5117 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Tue, 26 Nov 2024 14:50:10 +0100 Subject: [PATCH] feat: Add a way to add and remove text blocks ...not rendered on the canvas yet. --- app/components/chart-panel-layout-grid.tsx | 14 ++-- app/components/react-grid.tsx | 26 ++++--- app/config-types.ts | 29 +++++--- .../components/add-dataset-dialog.mock.ts | 1 + app/configurator/components/configurator.tsx | 3 + .../components/layout-configurator.tsx | 71 +++++++++++++++++-- .../configurator-state/context.tsx | 5 +- .../configurator-state/initial.tsx | 1 + app/configurator/configurator-state/mocks.ts | 2 + app/docs/charts.stories.tsx | 8 +++ app/docs/fixtures.ts | 1 + app/docs/lines.stories.tsx | 3 +- app/utils/chart-config/constants.ts | 2 +- app/utils/chart-config/versioning.ts | 57 +++++++++++++++ 14 files changed, 187 insertions(+), 36 deletions(-) diff --git a/app/components/chart-panel-layout-grid.tsx b/app/components/chart-panel-layout-grid.tsx index 055c6879d..4fda0eb28 100644 --- a/app/components/chart-panel-layout-grid.tsx +++ b/app/components/chart-panel-layout-grid.tsx @@ -33,26 +33,30 @@ const decodeLayouts = (layouts: Layouts) => { ); }; -export const ChartPanelLayoutCanvas = (props: ChartPanelLayoutTypeProps) => { - const { chartConfigs, renderChart, className } = props; +export const ChartPanelLayoutCanvas = ({ + chartConfigs, + renderChart, + className, +}: ChartPanelLayoutTypeProps) => { const [state, dispatch] = useConfiguratorState(hasChartConfigs); + const layout = state.layout; const [layouts, setLayouts] = useState(() => { assert( - state.layout.type === "dashboard" && state.layout.layout === "canvas", + layout.type === "dashboard" && layout.layout === "canvas", "ChartPanelLayoutGrid should be rendered only for dashboard layout with canvas" ); - return state.layout.layouts; + return layout.layouts; }); const handleChangeLayouts = (layouts: Layouts) => { - const layout = state.layout; assert( layout.type === "dashboard" && layout.layout === "canvas", "ChartPanelLayoutGrid should be rendered only for dashboard layout with canvas" ); const parsedLayouts = decodeLayouts(layouts); + if (!parsedLayouts) { return; } diff --git a/app/components/react-grid.tsx b/app/components/react-grid.tsx index 1be7e98fe..1dc531afc 100644 --- a/app/components/react-grid.tsx +++ b/app/components/react-grid.tsx @@ -226,9 +226,9 @@ export const ChartGridLayout = ({ } & ComponentProps) => { const classes = useStyles(); const [state, dispatch] = useConfiguratorState(hasChartConfigs); - const configLayout = state.layout; + const layout = state.layout; assert( - configLayout.type === "dashboard" && configLayout.layout === "canvas", + layout.type === "dashboard" && layout.layout === "canvas", "ChartGridLayout can only be used in a canvas layout!" ); const allowHeightInitialization = isLayouting(state); @@ -268,7 +268,10 @@ export const ChartGridLayout = ({ return [ breakpoint, chartLayouts.map((chartLayout) => { - if (configLayout.layoutsMetadata[chartLayout.i]?.initialized) { + if ( + layout.blocks.find((block) => block.key === chartLayout.i) + ?.initialized + ) { return chartLayout; } @@ -315,14 +318,15 @@ export const ChartGridLayout = ({ dispatch({ type: "LAYOUT_CHANGED", value: { - ...configLayout, + ...layout, layouts: newLayouts, - layoutsMetadata: Object.fromEntries( - state.chartConfigs.map(({ key }) => { - const layoutMetadata = configLayout.layoutsMetadata[key]; - return [key, { ...layoutMetadata, initialized: true }]; - }) - ), + blocks: layout.blocks.map((block) => { + return { + ...block, + // TODO: initialize other block types + initialized: block.type === "chart" ? true : block.initialized, + }; + }), }, }); } @@ -333,7 +337,7 @@ export const ChartGridLayout = ({ enhancedLayouts, mountedForSomeTime, resize, - configLayout, + layout, state.chartConfigs, ]); diff --git a/app/config-types.ts b/app/config-types.ts index 7822e1af2..353cda18f 100644 --- a/app/config-types.ts +++ b/app/config-types.ts @@ -1136,23 +1136,31 @@ export const ReactGridLayoutsType = t.record( ); export type ReactGridLayoutsType = t.TypeOf; -const ReactGridLayoutMetadata = t.type({ - initialized: t.boolean, +const LayoutChartBlock = t.type({ + type: t.literal("chart"), + key: t.string, }); -export type ReactGridLayoutMetadata = t.TypeOf; +export type LayoutChartBlock = t.TypeOf; -const ReactGridLayoutsMetadataType = t.record( - t.string, - ReactGridLayoutMetadata -); -export type ReactGridLayoutsMetadataType = t.TypeOf< - typeof ReactGridLayoutsMetadataType ->; +const LayoutTextBlock = t.type({ + type: t.literal("text"), + key: t.string, + title: t.string, + description: t.string, +}); +export type LayoutTextBlock = t.TypeOf; + +const LayoutBlock = t.intersection([ + t.union([LayoutChartBlock, LayoutTextBlock]), + t.type({ initialized: t.boolean }), +]); +export type LayoutBlock = t.TypeOf; const Layout = t.intersection([ t.type({ activeField: t.union([t.undefined, t.string]), meta: Meta, + blocks: t.array(LayoutBlock), }), t.union([ t.type({ @@ -1166,7 +1174,6 @@ const Layout = t.intersection([ type: t.literal("dashboard"), layout: t.literal("canvas"), layouts: ReactGridLayoutsType, - layoutsMetadata: ReactGridLayoutsMetadataType, }), t.type({ type: t.literal("singleURLs"), diff --git a/app/configurator/components/add-dataset-dialog.mock.ts b/app/configurator/components/add-dataset-dialog.mock.ts index 078e166a5..aa1b32752 100644 --- a/app/configurator/components/add-dataset-dialog.mock.ts +++ b/app/configurator/components/add-dataset-dialog.mock.ts @@ -10,6 +10,7 @@ export const photovoltaikChartStateMock: ConfiguratorStateConfiguringChart = { layout: { activeField: "y", type: "tab", + blocks: [{ type: "chart", key: "8-5RW138pTDA", initialized: true }], meta: { title: { de: "", diff --git a/app/configurator/components/configurator.tsx b/app/configurator/components/configurator.tsx index cfd4963a5..db47989cd 100644 --- a/app/configurator/components/configurator.tsx +++ b/app/configurator/components/configurator.tsx @@ -598,6 +598,7 @@ const LayoutingStep = () => { value: { type: "tab", meta: state.layout.meta, + blocks: state.layout.blocks, activeField: undefined, }, }); @@ -614,6 +615,7 @@ const LayoutingStep = () => { value: { type: "dashboard", meta: state.layout.meta, + blocks: state.layout.blocks, layout: "tall", activeField: undefined, }, @@ -634,6 +636,7 @@ const LayoutingStep = () => { (chartConfig) => chartConfig.key ), meta: state.layout.meta, + blocks: state.layout.blocks, activeField: undefined, }, }); diff --git a/app/configurator/components/layout-configurator.tsx b/app/configurator/components/layout-configurator.tsx index 140dd41dc..f57de17c9 100644 --- a/app/configurator/components/layout-configurator.tsx +++ b/app/configurator/components/layout-configurator.tsx @@ -1,6 +1,7 @@ import { t, Trans } from "@lingui/macro"; import { Box, + IconButton as MUIIconButton, Stack, Switch, SwitchProps, @@ -55,8 +56,10 @@ import { } from "@/domain/data"; import { useTimeFormatLocale, useTimeFormatUnit } from "@/formatters"; import { useConfigsCubeComponents } from "@/graphql/hooks"; +import { Icon } from "@/icons"; import { useLocale } from "@/src"; import { useDashboardInteractiveFilters } from "@/stores/interactive-filters"; +import { createId } from "@/utils/create-id"; import { getTimeFilterOptions } from "@/utils/time-filter-options"; export const LayoutConfigurator = () => { @@ -541,8 +544,38 @@ const DashboardTimeRangeFilterOptions = ({ }; const LayoutBlocksConfigurator = () => { - const [state] = useConfiguratorState(isLayouting); + const [state, dispatch] = useConfiguratorState(isLayouting); const { layout } = state; + const { blocks } = layout; + + const handleAddTextBlock = useEventCallback(() => { + dispatch({ + type: "LAYOUT_CHANGED", + value: { + ...layout, + blocks: [ + ...layout.blocks, + { + type: "text", + key: createId(), + title: "", + description: "", + initialized: false, + }, + ], + }, + }); + }); + + const handleRemoveBlock = useEventCallback((key: string) => { + dispatch({ + type: "LAYOUT_CHANGED", + value: { + ...layout, + blocks: layout.blocks.filter((b) => b.key !== key), + }, + }); + }); return layout.type === "dashboard" && layout.layout === "canvas" ? ( @@ -550,7 +583,31 @@ const LayoutBlocksConfigurator = () => { Text elements - + + {blocks + .filter((b) => b.type === "text") + .map((block) => ( + + {block.key} + handleRemoveBlock(block.key)} + > + + + + ))} + + Add text @@ -586,9 +643,13 @@ const migrateLayout = ( ...layout, layout: newLayoutType, layouts, - layoutsMetadata: Object.fromEntries( - chartConfigs.map(({ key }) => [key, { initialized: false }]) - ), + blocks: chartConfigs.map(({ key }) => { + return { + type: "chart", + key, + initialized: false, + }; + }), }; } else { return { diff --git a/app/configurator/configurator-state/context.tsx b/app/configurator/configurator-state/context.tsx index 322d9d696..ef4fca709 100644 --- a/app/configurator/configurator-state/context.tsx +++ b/app/configurator/configurator-state/context.tsx @@ -191,7 +191,7 @@ export async function publishState( ) { switch (state.layout.type) { case "singleURLs": - const { publishableChartKeys, meta } = state.layout; + const { publishableChartKeys, meta, blocks } = state.layout; const reversedChartKeys = publishableChartKeys.slice().reverse(); // Charts are published in order, keep the current tab open with first chart @@ -203,7 +203,8 @@ export async function publishState( // Ensure that the layout is reset to single-chart mode layout: { type: "tab", - meta: meta, + meta, + blocks, activeField: undefined, }, }, diff --git a/app/configurator/configurator-state/initial.tsx b/app/configurator/configurator-state/initial.tsx index b35e93924..4b7ff9d1e 100644 --- a/app/configurator/configurator-state/initial.tsx +++ b/app/configurator/configurator-state/initial.tsx @@ -39,6 +39,7 @@ export const getInitialConfiguringConfigBasedOnCube = (props: { it: "", }, }, + blocks: [{ type: "chart", key: chartConfig.key, initialized: false }], activeField: undefined, }, chartConfigs: [chartConfig], diff --git a/app/configurator/configurator-state/mocks.ts b/app/configurator/configurator-state/mocks.ts index 589c9f9b4..f55ded037 100644 --- a/app/configurator/configurator-state/mocks.ts +++ b/app/configurator/configurator-state/mocks.ts @@ -22,6 +22,7 @@ export const configStateMock = { type: "singleURLs", publishableChartKeys: [], meta: {} as ConfiguratorStateConfiguringChart["layout"]["meta"], + blocks: [{ type: "chart", key: "abc", initialized: true }], }, chartConfigs: [ { @@ -101,6 +102,7 @@ export const configStateMock = { it: "", }, }, + blocks: [{ type: "chart", key: "2of7iJAjccuj", initialized: true }], }, chartConfigs: [ { diff --git a/app/docs/charts.stories.tsx b/app/docs/charts.stories.tsx index d4118c2af..a3a0a7316 100644 --- a/app/docs/charts.stories.tsx +++ b/app/docs/charts.stories.tsx @@ -65,6 +65,7 @@ const ColumnsStory = { description: { en: "", de: "", fr: "", it: "" }, label: { en: "", de: "", fr: "", it: "" }, }, + blocks: [{ type: "chart", key: chartConfig.key, initialized: true }], activeField: undefined, }, chartConfigs: [chartConfig], @@ -129,6 +130,13 @@ const ScatterplotStory = { description: { en: "", de: "", fr: "", it: "" }, label: { en: "", de: "", fr: "", it: "" }, }, + blocks: [ + { + type: "chart", + key: scatterplotChartConfig.key, + initialized: true, + }, + ], activeField: undefined, }, chartConfigs: [chartConfig], diff --git a/app/docs/fixtures.ts b/app/docs/fixtures.ts index e151d4323..00eba1612 100644 --- a/app/docs/fixtures.ts +++ b/app/docs/fixtures.ts @@ -28,6 +28,7 @@ export const states: ConfiguratorState[] = [ description: { en: "", de: "", fr: "", it: "" }, label: { en: "", de: "", fr: "", it: "" }, }, + blocks: [{ type: "chart", key: "column", initialized: true }], activeField: undefined, }, chartConfigs: [ diff --git a/app/docs/lines.stories.tsx b/app/docs/lines.stories.tsx index 3a4b85a5e..1a67b8214 100644 --- a/app/docs/lines.stories.tsx +++ b/app/docs/lines.stories.tsx @@ -43,10 +43,11 @@ const LineChartStory = () => ( description: { en: "", de: "", fr: "", it: "" }, label: { en: "", de: "", fr: "", it: "" }, }, + blocks: [{ type: "chart", key: chartConfig.key, initialized: false }], activeField: undefined, }, chartConfigs: [chartConfig], - activeChartKey: "line", + activeChartKey: chartConfig.key, dashboardFilters: { timeRange: { active: false, diff --git a/app/utils/chart-config/constants.ts b/app/utils/chart-config/constants.ts index 4079da244..8427f0135 100644 --- a/app/utils/chart-config/constants.ts +++ b/app/utils/chart-config/constants.ts @@ -1,3 +1,3 @@ -export const CONFIGURATOR_STATE_VERSION = "4.0.0"; +export const CONFIGURATOR_STATE_VERSION = "4.1.0"; export const CHART_CONFIG_VERSION = "4.0.0"; diff --git a/app/utils/chart-config/versioning.ts b/app/utils/chart-config/versioning.ts index bbb7e479b..44b7fb9ac 100644 --- a/app/utils/chart-config/versioning.ts +++ b/app/utils/chart-config/versioning.ts @@ -1868,6 +1868,63 @@ export const configuratorStateMigrations: Migration[] = [ delete dataFilters.componentIds; newConfig.dashboardFilters.dataFilters = dataFilters; + return newConfig; + }, + }, + { + description: `ALL { + layout { + + blocks + - layoutsMetadata + } + }`, + from: "4.0.0", + to: "4.1.0", + up: async (config) => { + const newConfig = { ...config, version: "4.1.0" }; + + if (newConfig.layout.layoutsMetadata) { + newConfig.layout.blocks = Object.entries( + newConfig.layout.layoutsMetadata + ).map(([k, v]) => { + return { + type: "chart", + key: k, + ...(v as object), + }; + }); + delete newConfig.layout.layoutsMetadata; + } else { + newConfig.layout.blocks = newConfig.chartConfigs.map( + (chartConfig: any) => { + return { + type: "chart", + key: chartConfig.key, + initialized: false, + }; + } + ); + } + + return newConfig; + }, + down: async (config) => { + const newConfig = { ...config, version: "4.0.0" }; + + if ( + newConfig.layout.type === "dashboard" && + newConfig.layout.layout === "canvas" + ) { + newConfig.layout.layoutsMetadata = Object.fromEntries( + newConfig.layout.blocks.map((block: any) => { + const { key, initialized } = block; + return [key, { initialized }]; + }) + ); + } + + delete newConfig.layout.blocks; + return newConfig; }, },