Fix: Update chart from array to object (#3907)

* update structure of chart data

* update chart data

* update chart data structure

* remove array like validator

* remove log

* remove log

* widget utils + update tests for chart data validations

* update utils test

* add migrations

* remove unnecessary helper function

* remove unnecessary helper function

* update validation test cases

* WIP

* WIP 2

* Remove validationConfigMap from widget and add to properties

* Update data tree validator to get validation from the correct place

* Minor reference fixes

* Fix test mocks

* fix for bad setting of nested property path

* add test for migration for chart widget function

* fix test for widget utils

* remove unused import;

* remove unsued import

* update migration version

* remove console and add check if data is array

* fix custom fusion chart + validation not working issue

* move custom fusion chart config to chart data section

* remove console and unused import

* fix test

* fix property config test

* fix dynamicbinding path list in migration

* remove old chart validation

* remove old validation test

* fix widget prop utils test

* remove array codepath for widget utils

* fix utils test

* fix utils test

* fix prettier issue

Co-authored-by: Pawan Kumar <pawankumar@Pawans-MacBook-Pro.local>
Co-authored-by: root <root@DESKTOP-9GENCK0.localdomain>
Co-authored-by: hetunandu <hetu@appsmith.com>
Co-authored-by: Hetu Nandu <hetunandu@gmail.com>
This commit is contained in:
Pawan Kumar 2021-04-26 16:05:59 +05:30 committed by GitHub
parent 46b67577dd
commit d814e780ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 378 additions and 310 deletions

View File

@ -1,10 +1,10 @@
import _, { isString } from "lodash"; import _, { get } from "lodash";
import React from "react"; import React from "react";
import styled from "styled-components"; import styled from "styled-components";
import { getBorderCSSShorthand, invisible } from "constants/DefaultTheme"; import { getBorderCSSShorthand, invisible } from "constants/DefaultTheme";
import { getAppsmithConfigs } from "configs"; import { getAppsmithConfigs } from "configs";
import { ChartData, ChartDataPoint, ChartType } from "widgets/ChartWidget"; import { AllChartData, ChartDataPoint, ChartType } from "widgets/ChartWidget";
import log from "loglevel"; import log from "loglevel";
export interface CustomFusionChartConfig { export interface CustomFusionChartConfig {
@ -43,7 +43,7 @@ FusionCharts.options.license({
export interface ChartComponentProps { export interface ChartComponentProps {
chartType: ChartType; chartType: ChartType;
chartData: ChartData[]; chartData: AllChartData;
customFusionChartConfig: CustomFusionChartConfig; customFusionChartConfig: CustomFusionChartConfig;
xAxisName: string; xAxisName: string;
yAxisName: string; yAxisName: string;
@ -73,7 +73,8 @@ class ChartComponent extends React.Component<ChartComponentProps> {
getChartType = () => { getChartType = () => {
const { chartType, allowHorizontalScroll, chartData } = this.props; const { chartType, allowHorizontalScroll, chartData } = this.props;
const isMSChart = chartData.length > 1; const dataLength = Object.keys(chartData).length;
const isMSChart = dataLength > 1;
switch (chartType) { switch (chartType) {
case "PIE_CHART": case "PIE_CHART":
return "pie2d"; return "pie2d";
@ -107,9 +108,11 @@ class ChartComponent extends React.Component<ChartComponentProps> {
}; };
getChartData = () => { getChartData = () => {
const chartData: ChartData[] = this.props.chartData; const chartData: AllChartData = this.props.chartData;
const dataLength = Object.keys(chartData).length;
if (chartData.length === 0) { // if datalength is zero, just pass a empty datum
if (dataLength === 0) {
return [ return [
{ {
label: "", label: "",
@ -118,14 +121,13 @@ class ChartComponent extends React.Component<ChartComponentProps> {
]; ];
} }
let data: ChartDataPoint[] = chartData[0].data; const firstKey = Object.keys(chartData)[0] as string;
if (isString(chartData[0].data)) { let data = get(chartData, `${firstKey}.data`, []) as ChartDataPoint[];
try {
data = JSON.parse(chartData[0].data); if (!Array.isArray(data)) {
} catch (e) { data = [];
data = [];
}
} }
if (data.length === 0) { if (data.length === 0) {
return [ return [
{ {
@ -134,6 +136,7 @@ class ChartComponent extends React.Component<ChartComponentProps> {
}, },
]; ];
} }
return data.map((item) => { return data.map((item) => {
return { return {
label: item.x, label: item.x,
@ -142,22 +145,30 @@ class ChartComponent extends React.Component<ChartComponentProps> {
}); });
}; };
getChartCategoriesMutliSeries = (chartData: ChartData[]) => { getChartCategoriesMutliSeries = (chartData: AllChartData) => {
const categories: string[] = []; const categories: string[] = [];
for (let index = 0; index < chartData.length; index++) {
const data: ChartDataPoint[] = chartData[index].data; Object.keys(chartData).forEach((key: string) => {
let data = get(chartData, `${key}.data`, []) as ChartDataPoint[];
if (!Array.isArray(data)) {
data = [];
}
for (let dataIndex = 0; dataIndex < data.length; dataIndex++) { for (let dataIndex = 0; dataIndex < data.length; dataIndex++) {
const category = data[dataIndex].x; const category = data[dataIndex].x;
if (!categories.includes(category)) { if (!categories.includes(category)) {
categories.push(category); categories.push(category);
} }
} }
} });
return categories; return categories;
}; };
getChartCategories = (chartData: ChartData[]) => { getChartCategories = (chartData: AllChartData) => {
const categories: string[] = this.getChartCategoriesMutliSeries(chartData); const categories: string[] = this.getChartCategoriesMutliSeries(chartData);
if (categories.length === 0) { if (categories.length === 0) {
return [ return [
{ {
@ -192,9 +203,18 @@ class ChartComponent extends React.Component<ChartComponentProps> {
}); });
}; };
getChartDataset = (chartData: ChartData[]) => { /**
* creates dataset need by fusion chart from widget object-data
*
* @param chartData
* @returns
*/
getChartDataset = (chartData: AllChartData) => {
const categories: string[] = this.getChartCategoriesMutliSeries(chartData); const categories: string[] = this.getChartCategoriesMutliSeries(chartData);
return chartData.map((item: ChartData) => {
const dataset = Object.keys(chartData).map((key: string) => {
const item = get(chartData, `${key}`);
const seriesChartData: Array<Record< const seriesChartData: Array<Record<
string, string,
unknown unknown
@ -204,6 +224,8 @@ class ChartComponent extends React.Component<ChartComponentProps> {
data: seriesChartData, data: seriesChartData,
}; };
}); });
return dataset;
}; };
getChartConfig = () => { getChartConfig = () => {
@ -219,10 +241,9 @@ class ChartComponent extends React.Component<ChartComponentProps> {
}; };
getChartDataSource = () => { getChartDataSource = () => {
if ( const dataLength = Object.keys(this.props.chartData).length;
this.props.chartData.length <= 1 ||
this.props.chartType === "PIE_CHART" if (dataLength <= 1 || this.props.chartType === "PIE_CHART") {
) {
return { return {
chart: this.getChartConfig(), chart: this.getChartConfig(),
data: this.getChartData(), data: this.getChartData(),

View File

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import _ from "lodash"; import { get, has, isString } from "lodash";
import BaseControl, { ControlProps } from "./BaseControl"; import BaseControl, { ControlProps } from "./BaseControl";
import { ControlWrapper, StyledPropertyPaneButton } from "./StyledControls"; import { ControlWrapper, StyledPropertyPaneButton } from "./StyledControls";
import styled from "constants/DefaultTheme"; import styled from "constants/DefaultTheme";
@ -13,6 +13,8 @@ import {
TabBehaviour, TabBehaviour,
} from "components/editorComponents/CodeEditor/EditorConfig"; } from "components/editorComponents/CodeEditor/EditorConfig";
import { Size, Category } from "components/ads/Button"; import { Size, Category } from "components/ads/Button";
import { AllChartData, ChartData } from "widgets/ChartWidget";
import { generateReactKey } from "utils/generators";
const Wrapper = styled.div` const Wrapper = styled.div`
background-color: ${(props) => background-color: ${(props) =>
@ -76,16 +78,15 @@ const Box = styled.div`
`; `;
type RenderComponentProps = { type RenderComponentProps = {
index: number; index: string;
item: { item: ChartData;
seriesName: string;
data: Array<{ x: string; y: string }> | string;
};
length: number; length: number;
isValid: boolean; validationMessage: {
validationMessage: string; data: string;
deleteOption: (index: number) => void; seriesName: string;
updateOption: (index: number, key: string, value: string) => void; };
deleteOption: (index: string) => void;
updateOption: (index: string, key: string, value: string) => void;
evaluated: { evaluated: {
seriesName: string; seriesName: string;
data: Array<{ x: string; y: string }> | any; data: Array<{ x: string; y: string }> | any;
@ -100,9 +101,10 @@ function DataControlComponent(props: RenderComponentProps) {
item, item,
index, index,
length, length,
isValid,
evaluated, evaluated,
validationMessage,
} = props; } = props;
return ( return (
<StyledOptionControlWrapper orientation={"VERTICAL"}> <StyledOptionControlWrapper orientation={"VERTICAL"}>
<ActionHolder> <ActionHolder>
@ -160,7 +162,9 @@ function DataControlComponent(props: RenderComponentProps) {
}} }}
evaluatedValue={evaluated?.data} evaluatedValue={evaluated?.data}
meta={{ meta={{
error: isValid ? "" : "There is an error", error: has(validationMessage, "data")
? get(validationMessage, "data")
: "",
touched: true, touched: true,
}} }}
theme={props.theme} theme={props.theme}
@ -176,95 +180,55 @@ function DataControlComponent(props: RenderComponentProps) {
} }
class ChartDataControl extends BaseControl<ControlProps> { class ChartDataControl extends BaseControl<ControlProps> {
getValidations = (message: string, isValid: boolean, len: number) => {
const validations: Array<{
isValid: boolean;
validationMessage: string;
}> = [];
let index = -1;
let validationMessage = "";
if (message.indexOf("##") !== -1) {
const messages = message.split("##");
index = Number(messages[0]);
validationMessage = messages[1];
}
for (let i = 0; i < len; i++) {
if (i === index) {
validations.push({
isValid: false,
validationMessage: validationMessage,
});
} else {
validations.push({
isValid: true,
validationMessage: "",
});
}
}
return validations;
};
getEvaluatedValue = () => {
if (Array.isArray(this.props.evaluatedValue)) {
return this.props.evaluatedValue;
}
return [];
};
render() { render() {
const chartData: Array<{ seriesName: string; data: string }> = _.isString( const chartData: AllChartData = isString(this.props.propertyValue)
this.props.propertyValue, ? {}
)
? []
: this.props.propertyValue; : this.props.propertyValue;
const dataLength = chartData.length;
const { validationMessage, isValid } = this.props;
const validations: Array<{
isValid: boolean;
validationMessage: string;
}> = this.getValidations(
validationMessage || "",
isValid,
chartData.length,
);
const evaluatedValue = this.getEvaluatedValue(); const dataLength = Object.keys(chartData).length;
const { validationMessage } = this.props;
const evaluatedValue = this.props.evaluatedValue;
const firstKey = Object.keys(chartData)[0] as string;
if (this.props.widgetProperties.chartType === "PIE_CHART") { if (this.props.widgetProperties.chartType === "PIE_CHART") {
const data = chartData.length const data = dataLength
? chartData[0] ? get(chartData, `${firstKey}`)
: { : {
seriesName: "", seriesName: "",
data: "", data: [],
}; };
return ( return (
<DataControlComponent <DataControlComponent
index={0} index={firstKey}
item={data} item={data}
length={1} length={1}
deleteOption={this.deleteOption} deleteOption={this.deleteOption}
updateOption={this.updateOption} updateOption={this.updateOption}
isValid={validations[0].isValid} validationMessage={get(validationMessage, `${firstKey}`)}
validationMessage={validations[0].validationMessage} evaluated={get(evaluatedValue, `${firstKey}`)}
evaluated={evaluatedValue[0]}
theme={this.props.theme} theme={this.props.theme}
/> />
); );
} }
return ( return (
<React.Fragment> <React.Fragment>
<Wrapper> <Wrapper>
{chartData.map((data, index) => { {Object.keys(chartData).map((key: string) => {
const data = get(chartData, `${key}`);
return ( return (
<DataControlComponent <DataControlComponent
key={index} key={key}
index={index} index={key}
item={data} item={data}
length={dataLength} length={dataLength}
deleteOption={this.deleteOption} deleteOption={this.deleteOption}
updateOption={this.updateOption} updateOption={this.updateOption}
isValid={validations[index].isValid} validationMessage={get(validationMessage, `${key}`)}
validationMessage={validations[index].validationMessage} evaluated={get(evaluatedValue, `${key}`)}
evaluated={evaluatedValue[index]}
theme={this.props.theme} theme={this.props.theme}
/> />
); );
@ -284,27 +248,28 @@ class ChartDataControl extends BaseControl<ControlProps> {
); );
} }
deleteOption = (index: number) => { deleteOption = (index: string) => {
this.deleteProperties([`${this.props.propertyName}[${index}]`]); this.deleteProperties([`${this.props.propertyName}.${index}`]);
}; };
updateOption = ( updateOption = (
index: number, index: string,
propertyName: string, propertyName: string,
updatedValue: string, updatedValue: string,
) => { ) => {
this.updateProperty( this.updateProperty(
`${this.props.propertyName}[${index}].${propertyName}`, `${this.props.propertyName}.${index}.${propertyName}`,
updatedValue, updatedValue,
); );
}; };
/**
* it adds new series data object in the chartData
*/
addOption = () => { addOption = () => {
const chartData: Array<{ const randomString = generateReactKey();
seriesName: string;
data: string; this.updateProperty(`${this.props.propertyName}.${randomString}`, {
}> = this.props.propertyValue;
this.updateProperty(`${this.props.propertyName}[${chartData.length}]`, {
seriesName: "", seriesName: "",
data: JSON.stringify([{ x: "label", y: 50 }]), data: JSON.stringify([{ x: "label", y: 50 }]),
}); });

View File

@ -17,8 +17,8 @@ export enum VALIDATION_TYPES {
MIN_DATE = "MIN_DATE", MIN_DATE = "MIN_DATE",
MAX_DATE = "MAX_DATE", MAX_DATE = "MAX_DATE",
TABS_DATA = "TABS_DATA", TABS_DATA = "TABS_DATA",
CHART_DATA = "CHART_DATA",
LIST_DATA = "LIST_DATA", LIST_DATA = "LIST_DATA",
CHART_SERIES_DATA = "CHART_SERIES_DATA",
CUSTOM_FUSION_CHARTS_DATA = "CUSTOM_FUSION_CHARTS_DATA", CUSTOM_FUSION_CHARTS_DATA = "CUSTOM_FUSION_CHARTS_DATA",
MARKERS = "MARKERS", MARKERS = "MARKERS",
ACTION_SELECTOR = "ACTION_SELECTOR", ACTION_SELECTOR = "ACTION_SELECTOR",

View File

@ -186,12 +186,12 @@ describe("getAllPathsFromPropertyConfig", () => {
chartName: "Sales on working days", chartName: "Sales on working days",
allowHorizontalScroll: false, allowHorizontalScroll: false,
version: 1, version: 1,
chartData: [ chartData: {
{ "random-id": {
seriesName: "", seriesName: "",
data: "{{Api1.data}}", data: "{{Api1.data}}",
}, },
], },
xAxisName: "Last Week", xAxisName: "Last Week",
yAxisName: "Total Order Revenue $", yAxisName: "Total Order Revenue $",
type: WidgetTypes.CHART_WIDGET, type: WidgetTypes.CHART_WIDGET,
@ -206,7 +206,7 @@ describe("getAllPathsFromPropertyConfig", () => {
widgetId: "x1naz9is2b", widgetId: "x1naz9is2b",
dynamicBindingPathList: [ dynamicBindingPathList: [
{ {
key: "chartData[0].data", key: "chartData.random-id.data",
}, },
], ],
}; };
@ -216,8 +216,8 @@ describe("getAllPathsFromPropertyConfig", () => {
bindingPaths: { bindingPaths: {
chartType: EvaluationSubstitutionType.TEMPLATE, chartType: EvaluationSubstitutionType.TEMPLATE,
chartName: EvaluationSubstitutionType.TEMPLATE, chartName: EvaluationSubstitutionType.TEMPLATE,
"chartData[0].seriesName": EvaluationSubstitutionType.TEMPLATE, "chartData.random-id.seriesName": EvaluationSubstitutionType.TEMPLATE,
"chartData[0].data": EvaluationSubstitutionType.TEMPLATE, "chartData.random-id.data": EvaluationSubstitutionType.TEMPLATE,
xAxisName: EvaluationSubstitutionType.TEMPLATE, xAxisName: EvaluationSubstitutionType.TEMPLATE,
yAxisName: EvaluationSubstitutionType.TEMPLATE, yAxisName: EvaluationSubstitutionType.TEMPLATE,
isVisible: EvaluationSubstitutionType.TEMPLATE, isVisible: EvaluationSubstitutionType.TEMPLATE,
@ -226,8 +226,8 @@ describe("getAllPathsFromPropertyConfig", () => {
onDataPointClick: true, onDataPointClick: true,
}, },
validationPaths: { validationPaths: {
"chartData[0].data": "CHART_DATA", "chartData.random-id.data": "CHART_SERIES_DATA",
"chartData[0].seriesName": "TEXT", "chartData.random-id.seriesName": "TEXT",
chartName: "TEXT", chartName: "TEXT",
isVisible: "BOOLEAN", isVisible: "BOOLEAN",
xAxisName: "TEXT", xAxisName: "TEXT",

View File

@ -1,6 +1,6 @@
import { WidgetProps } from "widgets/BaseWidget"; import { WidgetProps } from "widgets/BaseWidget";
import { PropertyPaneConfig } from "constants/PropertyControlConstants"; import { PropertyPaneConfig } from "constants/PropertyControlConstants";
import { get } from "lodash"; import { get, isObject, isUndefined } from "lodash";
import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer"; import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer";
import { VALIDATION_TYPES } from "constants/WidgetValidation"; import { VALIDATION_TYPES } from "constants/WidgetValidation";
import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory"; import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory";
@ -29,6 +29,7 @@ export const getAllPathsFromPropertyConfig = (
if ("hidden" in controlConfig) { if ("hidden" in controlConfig) {
isHidden = controlConfig.hidden(widget, basePath); isHidden = controlConfig.hidden(widget, basePath);
} }
if (!isHidden) { if (!isHidden) {
if ( if (
controlConfig.isBindProperty && controlConfig.isBindProperty &&
@ -101,34 +102,36 @@ export const getAllPathsFromPropertyConfig = (
} }
} }
if (controlConfig.children) { if (controlConfig.children) {
// Property in array structure
const basePropertyPath = controlConfig.propertyName; const basePropertyPath = controlConfig.propertyName;
const widgetPropertyValue = get(widget, basePropertyPath, []); const widgetPropertyValue = get(widget, basePropertyPath, []);
if (Array.isArray(widgetPropertyValue)) { // Property in object structure
widgetPropertyValue.forEach( if (
(arrayPropertyValue: any, index: number) => { !isUndefined(widgetPropertyValue) &&
const arrayIndexPropertyPath = `${basePropertyPath}[${index}]`; isObject(widgetPropertyValue)
controlConfig.children.forEach((childPropertyConfig: any) => { ) {
const childArrayPropertyPath = `${arrayIndexPropertyPath}.${childPropertyConfig.propertyName}`; Object.keys(widgetPropertyValue).map((key: string) => {
if ( const objectIndexPropertyPath = `${basePropertyPath}.${key}`;
childPropertyConfig.isBindProperty && controlConfig.children.forEach((childPropertyConfig: any) => {
!childPropertyConfig.isTriggerProperty const childArrayPropertyPath = `${objectIndexPropertyPath}.${childPropertyConfig.propertyName}`;
) {
bindingPaths[childArrayPropertyPath] = if (
EvaluationSubstitutionType.TEMPLATE; childPropertyConfig.isBindProperty &&
if (childPropertyConfig.validation) { !childPropertyConfig.isTriggerProperty
validationPaths[childArrayPropertyPath] = ) {
childPropertyConfig.validation; bindingPaths[childArrayPropertyPath] =
} EvaluationSubstitutionType.TEMPLATE;
} else if ( if (childPropertyConfig.validation) {
childPropertyConfig.isBindProperty && validationPaths[childArrayPropertyPath] =
childPropertyConfig.isTriggerProperty childPropertyConfig.validation;
) {
triggerPaths[childArrayPropertyPath] = true;
} }
}); } else if (
}, childPropertyConfig.isBindProperty &&
); childPropertyConfig.isTriggerProperty
) {
triggerPaths[childArrayPropertyPath] = true;
}
});
});
} }
} }
}); });

View File

@ -384,8 +384,8 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
chartName: "Sales on working days", chartName: "Sales on working days",
allowHorizontalScroll: false, allowHorizontalScroll: false,
version: 1, version: 1,
chartData: [ chartData: {
{ [generateReactKey()]: {
seriesName: "Sales", seriesName: "Sales",
data: [ data: [
{ {
@ -418,7 +418,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
}, },
], ],
}, },
], },
xAxisName: "Last Week", xAxisName: "Last Week",
yAxisName: "Total Order Revenue $", yAxisName: "Total Order Revenue $",
}, },

View File

@ -0,0 +1,90 @@
import * as generators from "../utils/generators";
import { RenderModes, WidgetTypes } from "constants/WidgetConstants";
import { migrateChartDataFromArrayToObject } from "./WidgetPropsUtils";
describe("WidgetProps tests", () => {
it("it checks if array to object migration functions for chart widget ", () => {
const input = {
type: WidgetTypes.CANVAS_WIDGET,
widgetId: "0",
widgetName: "canvas",
parentColumnSpace: 1,
parentRowSpace: 1,
leftColumn: 0,
rightColumn: 0,
topRow: 0,
bottomRow: 0,
version: 17,
isLoading: false,
renderMode: RenderModes.CANVAS,
children: [
{
widgetId: "some-random-id",
widgetName: "chart1",
parentColumnSpace: 1,
parentRowSpace: 1,
leftColumn: 0,
rightColumn: 0,
topRow: 0,
bottomRow: 0,
version: 17,
isLoading: false,
renderMode: RenderModes.CANVAS,
type: WidgetTypes.CHART_WIDGET,
chartData: [
{
seriesName: "seris1",
data: [{ x: 1, y: 2 }],
},
],
},
],
};
// mocking implementation of our generateReactKey function
const generatorReactKeyMock = jest.spyOn(generators, "generateReactKey");
generatorReactKeyMock.mockImplementation(() => "some-random-key");
const result = migrateChartDataFromArrayToObject(input);
const output = {
type: WidgetTypes.CANVAS_WIDGET,
widgetId: "0",
widgetName: "canvas",
parentColumnSpace: 1,
parentRowSpace: 1,
leftColumn: 0,
rightColumn: 0,
topRow: 0,
bottomRow: 0,
version: 17,
isLoading: false,
renderMode: RenderModes.CANVAS,
children: [
{
widgetId: "some-random-id",
widgetName: "chart1",
parentColumnSpace: 1,
parentRowSpace: 1,
leftColumn: 0,
rightColumn: 0,
topRow: 0,
bottomRow: 0,
version: 17,
isLoading: false,
renderMode: RenderModes.CANVAS,
type: WidgetTypes.CHART_WIDGET,
dynamicBindingPathList: [],
chartData: {
"some-random-key": {
seriesName: "seris1",
data: [{ x: 1, y: 2 }],
},
},
},
],
};
expect(result).toStrictEqual(output);
});
});

View File

@ -21,7 +21,7 @@ import defaultTemplate from "templates/default";
import { generateReactKey } from "./generators"; import { generateReactKey } from "./generators";
import { ChartDataPoint } from "widgets/ChartWidget"; import { ChartDataPoint } from "widgets/ChartWidget";
import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer"; import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer";
import { isString } from "lodash"; import { isString, set } from "lodash";
import log from "loglevel"; import log from "loglevel";
import { import {
migrateTablePrimaryColumnsBindings, migrateTablePrimaryColumnsBindings,
@ -354,6 +354,62 @@ function migrateOldChartData(currentDSL: ContainerWidgetProps<WidgetProps>) {
return currentDSL; return currentDSL;
} }
/**
* changes chartData which we were using as array. now it will be a object
*
*
* @param currentDSL
* @returns
*/
export function migrateChartDataFromArrayToObject(
currentDSL: ContainerWidgetProps<WidgetProps>,
) {
currentDSL.children = currentDSL.children?.map((children: WidgetProps) => {
if (children.type === WidgetTypes.CHART_WIDGET) {
if (Array.isArray(children.chartData)) {
const newChartData = {};
const dynamicBindingPathList = children?.dynamicBindingPathList
? children?.dynamicBindingPathList.slice()
: [];
children.chartData.map((datum: any, index: number) => {
const generatedKey = generateReactKey();
set(newChartData, `${generatedKey}`, datum);
if (
Array.isArray(children.dynamicBindingPathList) &&
children.dynamicBindingPathList?.findIndex(
(path) => (path.key = `chartData[${index}].data`),
) > -1
) {
const foundIndex = children.dynamicBindingPathList.findIndex(
(path) => (path.key = `chartData[${index}].data`),
);
dynamicBindingPathList[foundIndex] = {
key: `chartData.${generatedKey}.data`,
};
}
});
children.dynamicBindingPathList = dynamicBindingPathList;
children.chartData = newChartData;
}
} else if (
children.type === WidgetTypes.CONTAINER_WIDGET ||
children.type === WidgetTypes.FORM_WIDGET ||
children.type === WidgetTypes.CANVAS_WIDGET ||
children.type === WidgetTypes.TABS_WIDGET
) {
children = migrateChartDataFromArrayToObject(children);
}
return children;
});
return currentDSL;
}
export const calculateDynamicHeight = ( export const calculateDynamicHeight = (
canvasWidgets: { canvasWidgets: {
[widgetId: string]: FlattenedWidgetProps; [widgetId: string]: FlattenedWidgetProps;
@ -482,6 +538,11 @@ const transformDSL = (currentDSL: ContainerWidgetProps<WidgetProps>) => {
currentDSL.version = 16; currentDSL.version = 16;
} }
if (currentDSL.version === 16) {
currentDSL = migrateChartDataFromArrayToObject(currentDSL);
currentDSL.version = 17;
}
return currentDSL; return currentDSL;
}; };

View File

@ -81,6 +81,9 @@ export interface ChartDataPoint {
y: any; y: any;
} }
export interface AllChartData {
[key: string]: ChartData;
}
export interface ChartData { export interface ChartData {
seriesName?: string; seriesName?: string;
data: ChartDataPoint[]; data: ChartDataPoint[];
@ -88,7 +91,7 @@ export interface ChartData {
export interface ChartWidgetProps extends WidgetProps, WithMeta { export interface ChartWidgetProps extends WidgetProps, WithMeta {
chartType: ChartType; chartType: ChartType;
chartData: ChartData[]; chartData: AllChartData;
customFusionChartConfig: { config: CustomFusionChartConfig }; customFusionChartConfig: { config: CustomFusionChartConfig };
xAxisName: string; xAxisName: string;
yAxisName: string; yAxisName: string;

View File

@ -68,14 +68,20 @@ describe("Validate Chart Widget's property config", () => {
}); });
it("Validates config when chartType is CUSTOM_FUSION_CHART", () => { it("Validates config when chartType is CUSTOM_FUSION_CHART", () => {
const hiddenFn: (props: any) => boolean = get(config, "[1].hidden"); const hiddenFn: (props: any) => boolean = get(
config,
"[1].children.[0].hidden",
);
let result = true; let result = true;
if (hiddenFn) result = hiddenFn({ chartType: "CUSTOM_FUSION_CHART" }); if (hiddenFn) result = hiddenFn({ chartType: "CUSTOM_FUSION_CHART" });
expect(result).toBeFalsy(); expect(result).toBeFalsy();
}); });
it("Validates that sections are hidden when chartType is CUSTOM_FUSION_CHART", () => { it("Validates that sections are hidden when chartType is CUSTOM_FUSION_CHART", () => {
const hiddenFns = [get(config, "[2].hidden"), get(config, "[3].hidden")]; const hiddenFns = [
get(config, "[1].children.[1].hidden"),
get(config, "[2].hidden"),
];
hiddenFns.forEach((fn: (props: any) => boolean) => { hiddenFns.forEach((fn: (props: any) => boolean) => {
const result = fn({ chartType: "CUSTOM_FUSION_CHART" }); const result = fn({ chartType: "CUSTOM_FUSION_CHART" });
expect(result).toBeTruthy(); expect(result).toBeTruthy();

View File

@ -62,32 +62,32 @@ export default [
}, },
], ],
}, },
{
helpText:
"Manually configure a FusionChart, see https://www.fusioncharts.com",
propertyName: "customFusionChartConfig",
placeholderText: `Enter {type: "bar2d","dataSource": {}}`,
label: "Custom Fusion Chart Configuration",
controlType: "CUSTOM_FUSION_CHARTS_DATA",
isBindProperty: true,
isTriggerProperty: false,
hidden: (x: any) => x.chartType !== "CUSTOM_FUSION_CHART",
validation: VALIDATION_TYPES.CUSTOM_FUSION_CHARTS_DATA,
},
{ {
sectionName: "Chart Data", sectionName: "Chart Data",
hidden: (props: ChartWidgetProps) =>
props.chartType === "CUSTOM_FUSION_CHART",
children: [ children: [
{
helpText:
"Manually configure a FusionChart, see https://www.fusioncharts.com",
placeholderText: `Enter {type: "bar2d","dataSource": {}}`,
propertyName: "customFusionChartConfig",
label: "Custom Fusion Chart Configuration",
controlType: "CUSTOM_FUSION_CHARTS_DATA",
isBindProperty: true,
isTriggerProperty: false,
validation: VALIDATION_TYPES.CUSTOM_FUSION_CHARTS_DATA,
hidden: (props: ChartWidgetProps) =>
props.chartType !== "CUSTOM_FUSION_CHART",
},
{ {
helpText: "Populates the chart with the data", helpText: "Populates the chart with the data",
propertyName: "chartData", propertyName: "chartData",
placeholderText: 'Enter [{ "x": "val", "y": "val" }]', placeholderText: 'Enter [{ "x": "val", "y": "val" }]',
label: "Chart Series", label: "Chart Series",
controlType: "CHART_DATA", controlType: "CHART_DATA",
isBindProperty: false, isBindProperty: false,
isTriggerProperty: false, isTriggerProperty: false,
hidden: (props: ChartWidgetProps) =>
props.chartType === "CUSTOM_FUSION_CHART",
children: [ children: [
{ {
helpText: "Series Name", helpText: "Series Name",
@ -105,7 +105,7 @@ export default [
controlType: "INPUT_TEXT_AREA", controlType: "INPUT_TEXT_AREA",
isBindProperty: true, isBindProperty: true,
isTriggerProperty: false, isTriggerProperty: false,
validation: VALIDATION_TYPES.CHART_DATA, validation: VALIDATION_TYPES.CHART_SERIES_DATA,
}, },
], ],
}, },

View File

@ -404,6 +404,8 @@ export default class DataTreeEvaluator {
} else { } else {
evalPropertyValue = unEvalPropertyValue; evalPropertyValue = unEvalPropertyValue;
} }
// debugger;
if (isWidget(entity)) { if (isWidget(entity)) {
const widgetEntity = entity; const widgetEntity = entity;
const defaultPropertyMap = this.widgetConfigMap[widgetEntity.type] const defaultPropertyMap = this.widgetConfigMap[widgetEntity.type]

View File

@ -19,109 +19,47 @@ const DUMMY_WIDGET: WidgetProps = {
}; };
describe("Validate Validators", () => { describe("Validate Validators", () => {
const validator = VALIDATORS.CHART_DATA; it("correctly validates chart series data ", () => {
it("correctly validates chart data ", () => {
const cases = [ const cases = [
{ {
input: [ input: [{ x: "Jan", y: 1000 }],
{
seriesName: "Sales",
data: [{ x: "Jan", y: 1000 }],
},
],
output: { output: {
isValid: true, isValid: true,
parsed: [ parsed: [{ x: "Jan", y: 1000 }],
{ transformed: [{ x: "Jan", y: 1000 }],
seriesName: "Sales",
data: [{ x: "Jan", y: 1000 }],
},
],
transformed: [
{
seriesName: "Sales",
data: [{ x: "Jan", y: 1000 }],
},
],
}, },
}, },
{ {
input: [ input: [{ x: "Jan", y: 1000 }, { x: "Feb" }],
{
seriesName: "Sales",
data: [{ x: "Jan", y: 1000 }, { x: "Feb" }],
},
],
output: { output: {
isValid: false, isValid: false,
message: message:
'0##This value does not evaluate to type "Array<x:string, y:number>"', 'This value does not evaluate to type: [{ "x": "val", "y": "val" }]',
parsed: [ parsed: [],
{ transformed: [{ x: "Jan", y: 1000 }, { x: "Feb" }],
seriesName: "Sales",
data: [],
},
],
transformed: [
{
seriesName: "Sales",
data: [{ x: "Jan", y: 1000 }, { x: "Feb" }],
},
],
}, },
}, },
{ {
input: [ input: undefined,
{
seriesName: "Sales",
data: undefined,
},
{
seriesName: "Expenses",
data: [
{ x: "Jan", y: 1000 },
{ x: "Feb", y: 2000 },
],
},
],
output: { output: {
isValid: false, isValid: false,
message: message:
'0##This value does not evaluate to type "Array<x:string, y:number>"', 'This value does not evaluate to type: [{ "x": "val", "y": "val" }]',
parsed: [ parsed: [],
{ transformed: undefined,
seriesName: "Sales",
data: [],
},
{
seriesName: "Expenses",
data: [
{ x: "Jan", y: 1000 },
{ x: "Feb", y: 2000 },
],
},
],
transformed: [
{
seriesName: "Sales",
data: undefined,
},
{
seriesName: "Expenses",
data: [
{ x: "Jan", y: 1000 },
{ x: "Feb", y: 2000 },
],
},
],
}, },
}, },
]; ];
for (const testCase of cases) { for (const testCase of cases) {
const response = validator(testCase.input, DUMMY_WIDGET, {}); const response = VALIDATORS.CHART_SERIES_DATA(
testCase.input,
DUMMY_WIDGET,
{},
);
expect(response).toStrictEqual(testCase.output); expect(response).toStrictEqual(testCase.output);
} }
}); });
it("Correctly validates page number", () => { it("Correctly validates page number", () => {
const input = [0, -1, undefined, null, 2, "abcd", [], ""]; const input = [0, -1, undefined, null, 2, "abcd", [], ""];
const expected = [1, 1, 1, 1, 2, 1, 1, 1]; const expected = [1, 1, 1, 1, 2, 1, 1, 1];

View File

@ -201,6 +201,7 @@ export const VALIDATORS: Record<VALIDATION_TYPES, Validator> = {
if (isString(value)) { if (isString(value)) {
parsed = JSON.parse(parsed as string); parsed = JSON.parse(parsed as string);
} }
if (!Array.isArray(parsed)) { if (!Array.isArray(parsed)) {
return { return {
isValid: false, isValid: false,
@ -209,6 +210,7 @@ export const VALIDATORS: Record<VALIDATION_TYPES, Validator> = {
message: `${WIDGET_TYPE_VALIDATION_ERROR} "Array"`, message: `${WIDGET_TYPE_VALIDATION_ERROR} "Array"`,
}; };
} }
return { isValid: true, parsed, transformed: parsed }; return { isValid: true, parsed, transformed: parsed };
} catch (e) { } catch (e) {
return { return {
@ -331,80 +333,56 @@ export const VALIDATORS: Record<VALIDATION_TYPES, Validator> = {
} }
return { isValid, parsed }; return { isValid, parsed };
}, },
[VALIDATION_TYPES.CHART_DATA]: ( [VALIDATION_TYPES.CHART_SERIES_DATA]: (
value: any, value: any,
props: WidgetProps, props: WidgetProps,
dataTree?: DataTree, dataTree?: DataTree,
): ValidationResponse => { ): ValidationResponse => {
const { isValid, parsed } = VALIDATORS[VALIDATION_TYPES.ARRAY]( let parsed = [];
value, let transformed = [];
props, let isValid = false;
dataTree, let validationMessage = "";
);
try {
const validatedResponse: ValidationResponse = VALIDATORS[
VALIDATION_TYPES.ARRAY
](value, props, dataTree);
if (validatedResponse.isValid) {
isValid = every(
validatedResponse.parsed,
(chartPoint: { x: string; y: any }) => {
return (
isObject(chartPoint) &&
isString(chartPoint.x) &&
!isUndefined(chartPoint.y)
);
},
);
}
if (!isValid) {
parsed = [];
transformed = validatedResponse.transformed;
validationMessage = `${WIDGET_TYPE_VALIDATION_ERROR}: [{ "x": "val", "y": "val" }]`;
} else {
parsed = validatedResponse.parsed;
transformed = validatedResponse.parsed;
}
} catch (e) {
console.error(e);
}
if (!isValid) { if (!isValid) {
return {
isValid,
parsed,
transformed: parsed,
message: `${WIDGET_TYPE_VALIDATION_ERROR} "Array<x:string, y:number>"`,
};
}
let validationMessage = "";
let index = 0;
const parsedChartData = [];
let isValidChart = true;
for (const seriesData of parsed) {
let isValidSeries = false;
try {
const validatedResponse: {
isValid: boolean;
parsed: Array<unknown>;
message?: string;
} = VALIDATORS[VALIDATION_TYPES.ARRAY](
seriesData.data,
props,
dataTree,
);
if (validatedResponse.isValid) {
isValidSeries = every(
validatedResponse.parsed,
(chartPoint: { x: string; y: any }) => {
return (
isObject(chartPoint) &&
isString(chartPoint.x) &&
!isUndefined(chartPoint.y)
);
},
);
}
if (!isValidSeries) {
isValidChart = false;
parsedChartData.push({
...seriesData,
data: [],
});
validationMessage = `${index}##${WIDGET_TYPE_VALIDATION_ERROR} "Array<x:string, y:number>"`;
} else {
parsedChartData.push({
...seriesData,
data: validatedResponse.parsed,
});
}
} catch (e) {
console.error(e);
}
index++;
}
if (!isValidChart) {
return { return {
isValid: false, isValid: false,
parsed: parsedChartData, parsed: [],
transformed: parsed, transformed: transformed,
message: validationMessage, message: validationMessage,
}; };
} }
return { isValid, parsed: parsedChartData, transformed: parsedChartData };
return { isValid, parsed, transformed };
}, },
[VALIDATION_TYPES.CUSTOM_FUSION_CHARTS_DATA]: ( [VALIDATION_TYPES.CUSTOM_FUSION_CHARTS_DATA]: (
value: any, value: any,
@ -419,6 +397,7 @@ export const VALIDATORS: Record<VALIDATION_TYPES, Validator> = {
if (props.chartName && parsed.dataSource && parsed.dataSource.chart) { if (props.chartName && parsed.dataSource && parsed.dataSource.chart) {
parsed.dataSource.chart.caption = props.chartName; parsed.dataSource.chart.caption = props.chartName;
} }
if (!isValid) { if (!isValid) {
return { return {
isValid, isValid,