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:
parent
46b67577dd
commit
d814e780ed
|
|
@ -1,10 +1,10 @@
|
|||
import _, { isString } from "lodash";
|
||||
import _, { get } from "lodash";
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { getBorderCSSShorthand, invisible } from "constants/DefaultTheme";
|
||||
import { getAppsmithConfigs } from "configs";
|
||||
import { ChartData, ChartDataPoint, ChartType } from "widgets/ChartWidget";
|
||||
import { AllChartData, ChartDataPoint, ChartType } from "widgets/ChartWidget";
|
||||
import log from "loglevel";
|
||||
|
||||
export interface CustomFusionChartConfig {
|
||||
|
|
@ -43,7 +43,7 @@ FusionCharts.options.license({
|
|||
|
||||
export interface ChartComponentProps {
|
||||
chartType: ChartType;
|
||||
chartData: ChartData[];
|
||||
chartData: AllChartData;
|
||||
customFusionChartConfig: CustomFusionChartConfig;
|
||||
xAxisName: string;
|
||||
yAxisName: string;
|
||||
|
|
@ -73,7 +73,8 @@ class ChartComponent extends React.Component<ChartComponentProps> {
|
|||
|
||||
getChartType = () => {
|
||||
const { chartType, allowHorizontalScroll, chartData } = this.props;
|
||||
const isMSChart = chartData.length > 1;
|
||||
const dataLength = Object.keys(chartData).length;
|
||||
const isMSChart = dataLength > 1;
|
||||
switch (chartType) {
|
||||
case "PIE_CHART":
|
||||
return "pie2d";
|
||||
|
|
@ -107,9 +108,11 @@ class ChartComponent extends React.Component<ChartComponentProps> {
|
|||
};
|
||||
|
||||
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 [
|
||||
{
|
||||
label: "",
|
||||
|
|
@ -118,14 +121,13 @@ class ChartComponent extends React.Component<ChartComponentProps> {
|
|||
];
|
||||
}
|
||||
|
||||
let data: ChartDataPoint[] = chartData[0].data;
|
||||
if (isString(chartData[0].data)) {
|
||||
try {
|
||||
data = JSON.parse(chartData[0].data);
|
||||
} catch (e) {
|
||||
data = [];
|
||||
}
|
||||
const firstKey = Object.keys(chartData)[0] as string;
|
||||
let data = get(chartData, `${firstKey}.data`, []) as ChartDataPoint[];
|
||||
|
||||
if (!Array.isArray(data)) {
|
||||
data = [];
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return [
|
||||
{
|
||||
|
|
@ -134,6 +136,7 @@ class ChartComponent extends React.Component<ChartComponentProps> {
|
|||
},
|
||||
];
|
||||
}
|
||||
|
||||
return data.map((item) => {
|
||||
return {
|
||||
label: item.x,
|
||||
|
|
@ -142,22 +145,30 @@ class ChartComponent extends React.Component<ChartComponentProps> {
|
|||
});
|
||||
};
|
||||
|
||||
getChartCategoriesMutliSeries = (chartData: ChartData[]) => {
|
||||
getChartCategoriesMutliSeries = (chartData: AllChartData) => {
|
||||
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++) {
|
||||
const category = data[dataIndex].x;
|
||||
if (!categories.includes(category)) {
|
||||
categories.push(category);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return categories;
|
||||
};
|
||||
|
||||
getChartCategories = (chartData: ChartData[]) => {
|
||||
getChartCategories = (chartData: AllChartData) => {
|
||||
const categories: string[] = this.getChartCategoriesMutliSeries(chartData);
|
||||
|
||||
if (categories.length === 0) {
|
||||
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);
|
||||
return chartData.map((item: ChartData) => {
|
||||
|
||||
const dataset = Object.keys(chartData).map((key: string) => {
|
||||
const item = get(chartData, `${key}`);
|
||||
|
||||
const seriesChartData: Array<Record<
|
||||
string,
|
||||
unknown
|
||||
|
|
@ -204,6 +224,8 @@ class ChartComponent extends React.Component<ChartComponentProps> {
|
|||
data: seriesChartData,
|
||||
};
|
||||
});
|
||||
|
||||
return dataset;
|
||||
};
|
||||
|
||||
getChartConfig = () => {
|
||||
|
|
@ -219,10 +241,9 @@ class ChartComponent extends React.Component<ChartComponentProps> {
|
|||
};
|
||||
|
||||
getChartDataSource = () => {
|
||||
if (
|
||||
this.props.chartData.length <= 1 ||
|
||||
this.props.chartType === "PIE_CHART"
|
||||
) {
|
||||
const dataLength = Object.keys(this.props.chartData).length;
|
||||
|
||||
if (dataLength <= 1 || this.props.chartType === "PIE_CHART") {
|
||||
return {
|
||||
chart: this.getChartConfig(),
|
||||
data: this.getChartData(),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import _ from "lodash";
|
||||
import { get, has, isString } from "lodash";
|
||||
import BaseControl, { ControlProps } from "./BaseControl";
|
||||
import { ControlWrapper, StyledPropertyPaneButton } from "./StyledControls";
|
||||
import styled from "constants/DefaultTheme";
|
||||
|
|
@ -13,6 +13,8 @@ import {
|
|||
TabBehaviour,
|
||||
} from "components/editorComponents/CodeEditor/EditorConfig";
|
||||
import { Size, Category } from "components/ads/Button";
|
||||
import { AllChartData, ChartData } from "widgets/ChartWidget";
|
||||
import { generateReactKey } from "utils/generators";
|
||||
|
||||
const Wrapper = styled.div`
|
||||
background-color: ${(props) =>
|
||||
|
|
@ -76,16 +78,15 @@ const Box = styled.div`
|
|||
`;
|
||||
|
||||
type RenderComponentProps = {
|
||||
index: number;
|
||||
item: {
|
||||
seriesName: string;
|
||||
data: Array<{ x: string; y: string }> | string;
|
||||
};
|
||||
index: string;
|
||||
item: ChartData;
|
||||
length: number;
|
||||
isValid: boolean;
|
||||
validationMessage: string;
|
||||
deleteOption: (index: number) => void;
|
||||
updateOption: (index: number, key: string, value: string) => void;
|
||||
validationMessage: {
|
||||
data: string;
|
||||
seriesName: string;
|
||||
};
|
||||
deleteOption: (index: string) => void;
|
||||
updateOption: (index: string, key: string, value: string) => void;
|
||||
evaluated: {
|
||||
seriesName: string;
|
||||
data: Array<{ x: string; y: string }> | any;
|
||||
|
|
@ -100,9 +101,10 @@ function DataControlComponent(props: RenderComponentProps) {
|
|||
item,
|
||||
index,
|
||||
length,
|
||||
isValid,
|
||||
evaluated,
|
||||
validationMessage,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<StyledOptionControlWrapper orientation={"VERTICAL"}>
|
||||
<ActionHolder>
|
||||
|
|
@ -160,7 +162,9 @@ function DataControlComponent(props: RenderComponentProps) {
|
|||
}}
|
||||
evaluatedValue={evaluated?.data}
|
||||
meta={{
|
||||
error: isValid ? "" : "There is an error",
|
||||
error: has(validationMessage, "data")
|
||||
? get(validationMessage, "data")
|
||||
: "",
|
||||
touched: true,
|
||||
}}
|
||||
theme={props.theme}
|
||||
|
|
@ -176,95 +180,55 @@ function DataControlComponent(props: RenderComponentProps) {
|
|||
}
|
||||
|
||||
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() {
|
||||
const chartData: Array<{ seriesName: string; data: string }> = _.isString(
|
||||
this.props.propertyValue,
|
||||
)
|
||||
? []
|
||||
const chartData: AllChartData = isString(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") {
|
||||
const data = chartData.length
|
||||
? chartData[0]
|
||||
const data = dataLength
|
||||
? get(chartData, `${firstKey}`)
|
||||
: {
|
||||
seriesName: "",
|
||||
data: "",
|
||||
data: [],
|
||||
};
|
||||
|
||||
return (
|
||||
<DataControlComponent
|
||||
index={0}
|
||||
index={firstKey}
|
||||
item={data}
|
||||
length={1}
|
||||
deleteOption={this.deleteOption}
|
||||
updateOption={this.updateOption}
|
||||
isValid={validations[0].isValid}
|
||||
validationMessage={validations[0].validationMessage}
|
||||
evaluated={evaluatedValue[0]}
|
||||
validationMessage={get(validationMessage, `${firstKey}`)}
|
||||
evaluated={get(evaluatedValue, `${firstKey}`)}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Wrapper>
|
||||
{chartData.map((data, index) => {
|
||||
{Object.keys(chartData).map((key: string) => {
|
||||
const data = get(chartData, `${key}`);
|
||||
|
||||
return (
|
||||
<DataControlComponent
|
||||
key={index}
|
||||
index={index}
|
||||
key={key}
|
||||
index={key}
|
||||
item={data}
|
||||
length={dataLength}
|
||||
deleteOption={this.deleteOption}
|
||||
updateOption={this.updateOption}
|
||||
isValid={validations[index].isValid}
|
||||
validationMessage={validations[index].validationMessage}
|
||||
evaluated={evaluatedValue[index]}
|
||||
validationMessage={get(validationMessage, `${key}`)}
|
||||
evaluated={get(evaluatedValue, `${key}`)}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
);
|
||||
|
|
@ -284,27 +248,28 @@ class ChartDataControl extends BaseControl<ControlProps> {
|
|||
);
|
||||
}
|
||||
|
||||
deleteOption = (index: number) => {
|
||||
this.deleteProperties([`${this.props.propertyName}[${index}]`]);
|
||||
deleteOption = (index: string) => {
|
||||
this.deleteProperties([`${this.props.propertyName}.${index}`]);
|
||||
};
|
||||
|
||||
updateOption = (
|
||||
index: number,
|
||||
index: string,
|
||||
propertyName: string,
|
||||
updatedValue: string,
|
||||
) => {
|
||||
this.updateProperty(
|
||||
`${this.props.propertyName}[${index}].${propertyName}`,
|
||||
`${this.props.propertyName}.${index}.${propertyName}`,
|
||||
updatedValue,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* it adds new series data object in the chartData
|
||||
*/
|
||||
addOption = () => {
|
||||
const chartData: Array<{
|
||||
seriesName: string;
|
||||
data: string;
|
||||
}> = this.props.propertyValue;
|
||||
this.updateProperty(`${this.props.propertyName}[${chartData.length}]`, {
|
||||
const randomString = generateReactKey();
|
||||
|
||||
this.updateProperty(`${this.props.propertyName}.${randomString}`, {
|
||||
seriesName: "",
|
||||
data: JSON.stringify([{ x: "label", y: 50 }]),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ export enum VALIDATION_TYPES {
|
|||
MIN_DATE = "MIN_DATE",
|
||||
MAX_DATE = "MAX_DATE",
|
||||
TABS_DATA = "TABS_DATA",
|
||||
CHART_DATA = "CHART_DATA",
|
||||
LIST_DATA = "LIST_DATA",
|
||||
CHART_SERIES_DATA = "CHART_SERIES_DATA",
|
||||
CUSTOM_FUSION_CHARTS_DATA = "CUSTOM_FUSION_CHARTS_DATA",
|
||||
MARKERS = "MARKERS",
|
||||
ACTION_SELECTOR = "ACTION_SELECTOR",
|
||||
|
|
|
|||
|
|
@ -186,12 +186,12 @@ describe("getAllPathsFromPropertyConfig", () => {
|
|||
chartName: "Sales on working days",
|
||||
allowHorizontalScroll: false,
|
||||
version: 1,
|
||||
chartData: [
|
||||
{
|
||||
chartData: {
|
||||
"random-id": {
|
||||
seriesName: "",
|
||||
data: "{{Api1.data}}",
|
||||
},
|
||||
],
|
||||
},
|
||||
xAxisName: "Last Week",
|
||||
yAxisName: "Total Order Revenue $",
|
||||
type: WidgetTypes.CHART_WIDGET,
|
||||
|
|
@ -206,7 +206,7 @@ describe("getAllPathsFromPropertyConfig", () => {
|
|||
widgetId: "x1naz9is2b",
|
||||
dynamicBindingPathList: [
|
||||
{
|
||||
key: "chartData[0].data",
|
||||
key: "chartData.random-id.data",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -216,8 +216,8 @@ describe("getAllPathsFromPropertyConfig", () => {
|
|||
bindingPaths: {
|
||||
chartType: EvaluationSubstitutionType.TEMPLATE,
|
||||
chartName: EvaluationSubstitutionType.TEMPLATE,
|
||||
"chartData[0].seriesName": EvaluationSubstitutionType.TEMPLATE,
|
||||
"chartData[0].data": EvaluationSubstitutionType.TEMPLATE,
|
||||
"chartData.random-id.seriesName": EvaluationSubstitutionType.TEMPLATE,
|
||||
"chartData.random-id.data": EvaluationSubstitutionType.TEMPLATE,
|
||||
xAxisName: EvaluationSubstitutionType.TEMPLATE,
|
||||
yAxisName: EvaluationSubstitutionType.TEMPLATE,
|
||||
isVisible: EvaluationSubstitutionType.TEMPLATE,
|
||||
|
|
@ -226,8 +226,8 @@ describe("getAllPathsFromPropertyConfig", () => {
|
|||
onDataPointClick: true,
|
||||
},
|
||||
validationPaths: {
|
||||
"chartData[0].data": "CHART_DATA",
|
||||
"chartData[0].seriesName": "TEXT",
|
||||
"chartData.random-id.data": "CHART_SERIES_DATA",
|
||||
"chartData.random-id.seriesName": "TEXT",
|
||||
chartName: "TEXT",
|
||||
isVisible: "BOOLEAN",
|
||||
xAxisName: "TEXT",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { WidgetProps } from "widgets/BaseWidget";
|
||||
import { PropertyPaneConfig } from "constants/PropertyControlConstants";
|
||||
import { get } from "lodash";
|
||||
import { get, isObject, isUndefined } from "lodash";
|
||||
import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer";
|
||||
import { VALIDATION_TYPES } from "constants/WidgetValidation";
|
||||
import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory";
|
||||
|
|
@ -29,6 +29,7 @@ export const getAllPathsFromPropertyConfig = (
|
|||
if ("hidden" in controlConfig) {
|
||||
isHidden = controlConfig.hidden(widget, basePath);
|
||||
}
|
||||
|
||||
if (!isHidden) {
|
||||
if (
|
||||
controlConfig.isBindProperty &&
|
||||
|
|
@ -101,34 +102,36 @@ export const getAllPathsFromPropertyConfig = (
|
|||
}
|
||||
}
|
||||
if (controlConfig.children) {
|
||||
// Property in array structure
|
||||
const basePropertyPath = controlConfig.propertyName;
|
||||
const widgetPropertyValue = get(widget, basePropertyPath, []);
|
||||
if (Array.isArray(widgetPropertyValue)) {
|
||||
widgetPropertyValue.forEach(
|
||||
(arrayPropertyValue: any, index: number) => {
|
||||
const arrayIndexPropertyPath = `${basePropertyPath}[${index}]`;
|
||||
controlConfig.children.forEach((childPropertyConfig: any) => {
|
||||
const childArrayPropertyPath = `${arrayIndexPropertyPath}.${childPropertyConfig.propertyName}`;
|
||||
if (
|
||||
childPropertyConfig.isBindProperty &&
|
||||
!childPropertyConfig.isTriggerProperty
|
||||
) {
|
||||
bindingPaths[childArrayPropertyPath] =
|
||||
EvaluationSubstitutionType.TEMPLATE;
|
||||
if (childPropertyConfig.validation) {
|
||||
validationPaths[childArrayPropertyPath] =
|
||||
childPropertyConfig.validation;
|
||||
}
|
||||
} else if (
|
||||
childPropertyConfig.isBindProperty &&
|
||||
childPropertyConfig.isTriggerProperty
|
||||
) {
|
||||
triggerPaths[childArrayPropertyPath] = true;
|
||||
// Property in object structure
|
||||
if (
|
||||
!isUndefined(widgetPropertyValue) &&
|
||||
isObject(widgetPropertyValue)
|
||||
) {
|
||||
Object.keys(widgetPropertyValue).map((key: string) => {
|
||||
const objectIndexPropertyPath = `${basePropertyPath}.${key}`;
|
||||
controlConfig.children.forEach((childPropertyConfig: any) => {
|
||||
const childArrayPropertyPath = `${objectIndexPropertyPath}.${childPropertyConfig.propertyName}`;
|
||||
|
||||
if (
|
||||
childPropertyConfig.isBindProperty &&
|
||||
!childPropertyConfig.isTriggerProperty
|
||||
) {
|
||||
bindingPaths[childArrayPropertyPath] =
|
||||
EvaluationSubstitutionType.TEMPLATE;
|
||||
if (childPropertyConfig.validation) {
|
||||
validationPaths[childArrayPropertyPath] =
|
||||
childPropertyConfig.validation;
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
} else if (
|
||||
childPropertyConfig.isBindProperty &&
|
||||
childPropertyConfig.isTriggerProperty
|
||||
) {
|
||||
triggerPaths[childArrayPropertyPath] = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -384,8 +384,8 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
|
|||
chartName: "Sales on working days",
|
||||
allowHorizontalScroll: false,
|
||||
version: 1,
|
||||
chartData: [
|
||||
{
|
||||
chartData: {
|
||||
[generateReactKey()]: {
|
||||
seriesName: "Sales",
|
||||
data: [
|
||||
{
|
||||
|
|
@ -418,7 +418,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
|
|||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
xAxisName: "Last Week",
|
||||
yAxisName: "Total Order Revenue $",
|
||||
},
|
||||
|
|
|
|||
90
app/client/src/utils/WidgetPropsUtils.test.tsx
Normal file
90
app/client/src/utils/WidgetPropsUtils.test.tsx
Normal 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);
|
||||
});
|
||||
});
|
||||
|
|
@ -21,7 +21,7 @@ import defaultTemplate from "templates/default";
|
|||
import { generateReactKey } from "./generators";
|
||||
import { ChartDataPoint } from "widgets/ChartWidget";
|
||||
import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer";
|
||||
import { isString } from "lodash";
|
||||
import { isString, set } from "lodash";
|
||||
import log from "loglevel";
|
||||
import {
|
||||
migrateTablePrimaryColumnsBindings,
|
||||
|
|
@ -354,6 +354,62 @@ function migrateOldChartData(currentDSL: ContainerWidgetProps<WidgetProps>) {
|
|||
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 = (
|
||||
canvasWidgets: {
|
||||
[widgetId: string]: FlattenedWidgetProps;
|
||||
|
|
@ -482,6 +538,11 @@ const transformDSL = (currentDSL: ContainerWidgetProps<WidgetProps>) => {
|
|||
currentDSL.version = 16;
|
||||
}
|
||||
|
||||
if (currentDSL.version === 16) {
|
||||
currentDSL = migrateChartDataFromArrayToObject(currentDSL);
|
||||
currentDSL.version = 17;
|
||||
}
|
||||
|
||||
return currentDSL;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -81,6 +81,9 @@ export interface ChartDataPoint {
|
|||
y: any;
|
||||
}
|
||||
|
||||
export interface AllChartData {
|
||||
[key: string]: ChartData;
|
||||
}
|
||||
export interface ChartData {
|
||||
seriesName?: string;
|
||||
data: ChartDataPoint[];
|
||||
|
|
@ -88,7 +91,7 @@ export interface ChartData {
|
|||
|
||||
export interface ChartWidgetProps extends WidgetProps, WithMeta {
|
||||
chartType: ChartType;
|
||||
chartData: ChartData[];
|
||||
chartData: AllChartData;
|
||||
customFusionChartConfig: { config: CustomFusionChartConfig };
|
||||
xAxisName: string;
|
||||
yAxisName: string;
|
||||
|
|
|
|||
|
|
@ -68,14 +68,20 @@ describe("Validate Chart Widget's property config", () => {
|
|||
});
|
||||
|
||||
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;
|
||||
if (hiddenFn) result = hiddenFn({ chartType: "CUSTOM_FUSION_CHART" });
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
|
||||
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) => {
|
||||
const result = fn({ chartType: "CUSTOM_FUSION_CHART" });
|
||||
expect(result).toBeTruthy();
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
hidden: (props: ChartWidgetProps) =>
|
||||
props.chartType === "CUSTOM_FUSION_CHART",
|
||||
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",
|
||||
propertyName: "chartData",
|
||||
placeholderText: 'Enter [{ "x": "val", "y": "val" }]',
|
||||
label: "Chart Series",
|
||||
controlType: "CHART_DATA",
|
||||
|
||||
isBindProperty: false,
|
||||
isTriggerProperty: false,
|
||||
hidden: (props: ChartWidgetProps) =>
|
||||
props.chartType === "CUSTOM_FUSION_CHART",
|
||||
children: [
|
||||
{
|
||||
helpText: "Series Name",
|
||||
|
|
@ -105,7 +105,7 @@ export default [
|
|||
controlType: "INPUT_TEXT_AREA",
|
||||
isBindProperty: true,
|
||||
isTriggerProperty: false,
|
||||
validation: VALIDATION_TYPES.CHART_DATA,
|
||||
validation: VALIDATION_TYPES.CHART_SERIES_DATA,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -404,6 +404,8 @@ export default class DataTreeEvaluator {
|
|||
} else {
|
||||
evalPropertyValue = unEvalPropertyValue;
|
||||
}
|
||||
|
||||
// debugger;
|
||||
if (isWidget(entity)) {
|
||||
const widgetEntity = entity;
|
||||
const defaultPropertyMap = this.widgetConfigMap[widgetEntity.type]
|
||||
|
|
|
|||
|
|
@ -19,109 +19,47 @@ const DUMMY_WIDGET: WidgetProps = {
|
|||
};
|
||||
|
||||
describe("Validate Validators", () => {
|
||||
const validator = VALIDATORS.CHART_DATA;
|
||||
it("correctly validates chart data ", () => {
|
||||
it("correctly validates chart series data ", () => {
|
||||
const cases = [
|
||||
{
|
||||
input: [
|
||||
{
|
||||
seriesName: "Sales",
|
||||
data: [{ x: "Jan", y: 1000 }],
|
||||
},
|
||||
],
|
||||
input: [{ x: "Jan", y: 1000 }],
|
||||
output: {
|
||||
isValid: true,
|
||||
parsed: [
|
||||
{
|
||||
seriesName: "Sales",
|
||||
data: [{ x: "Jan", y: 1000 }],
|
||||
},
|
||||
],
|
||||
transformed: [
|
||||
{
|
||||
seriesName: "Sales",
|
||||
data: [{ x: "Jan", y: 1000 }],
|
||||
},
|
||||
],
|
||||
parsed: [{ x: "Jan", y: 1000 }],
|
||||
transformed: [{ x: "Jan", y: 1000 }],
|
||||
},
|
||||
},
|
||||
{
|
||||
input: [
|
||||
{
|
||||
seriesName: "Sales",
|
||||
data: [{ x: "Jan", y: 1000 }, { x: "Feb" }],
|
||||
},
|
||||
],
|
||||
input: [{ x: "Jan", y: 1000 }, { x: "Feb" }],
|
||||
output: {
|
||||
isValid: false,
|
||||
message:
|
||||
'0##This value does not evaluate to type "Array<x:string, y:number>"',
|
||||
parsed: [
|
||||
{
|
||||
seriesName: "Sales",
|
||||
data: [],
|
||||
},
|
||||
],
|
||||
transformed: [
|
||||
{
|
||||
seriesName: "Sales",
|
||||
data: [{ x: "Jan", y: 1000 }, { x: "Feb" }],
|
||||
},
|
||||
],
|
||||
'This value does not evaluate to type: [{ "x": "val", "y": "val" }]',
|
||||
parsed: [],
|
||||
transformed: [{ x: "Jan", y: 1000 }, { x: "Feb" }],
|
||||
},
|
||||
},
|
||||
{
|
||||
input: [
|
||||
{
|
||||
seriesName: "Sales",
|
||||
data: undefined,
|
||||
},
|
||||
{
|
||||
seriesName: "Expenses",
|
||||
data: [
|
||||
{ x: "Jan", y: 1000 },
|
||||
{ x: "Feb", y: 2000 },
|
||||
],
|
||||
},
|
||||
],
|
||||
input: undefined,
|
||||
output: {
|
||||
isValid: false,
|
||||
message:
|
||||
'0##This value does not evaluate to type "Array<x:string, y:number>"',
|
||||
parsed: [
|
||||
{
|
||||
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 },
|
||||
],
|
||||
},
|
||||
],
|
||||
'This value does not evaluate to type: [{ "x": "val", "y": "val" }]',
|
||||
parsed: [],
|
||||
transformed: undefined,
|
||||
},
|
||||
},
|
||||
];
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
it("Correctly validates page number", () => {
|
||||
const input = [0, -1, undefined, null, 2, "abcd", [], ""];
|
||||
const expected = [1, 1, 1, 1, 2, 1, 1, 1];
|
||||
|
|
|
|||
|
|
@ -201,6 +201,7 @@ export const VALIDATORS: Record<VALIDATION_TYPES, Validator> = {
|
|||
if (isString(value)) {
|
||||
parsed = JSON.parse(parsed as string);
|
||||
}
|
||||
|
||||
if (!Array.isArray(parsed)) {
|
||||
return {
|
||||
isValid: false,
|
||||
|
|
@ -209,6 +210,7 @@ export const VALIDATORS: Record<VALIDATION_TYPES, Validator> = {
|
|||
message: `${WIDGET_TYPE_VALIDATION_ERROR} "Array"`,
|
||||
};
|
||||
}
|
||||
|
||||
return { isValid: true, parsed, transformed: parsed };
|
||||
} catch (e) {
|
||||
return {
|
||||
|
|
@ -331,80 +333,56 @@ export const VALIDATORS: Record<VALIDATION_TYPES, Validator> = {
|
|||
}
|
||||
return { isValid, parsed };
|
||||
},
|
||||
[VALIDATION_TYPES.CHART_DATA]: (
|
||||
[VALIDATION_TYPES.CHART_SERIES_DATA]: (
|
||||
value: any,
|
||||
props: WidgetProps,
|
||||
dataTree?: DataTree,
|
||||
): ValidationResponse => {
|
||||
const { isValid, parsed } = VALIDATORS[VALIDATION_TYPES.ARRAY](
|
||||
value,
|
||||
props,
|
||||
dataTree,
|
||||
);
|
||||
let parsed = [];
|
||||
let transformed = [];
|
||||
let isValid = false;
|
||||
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) {
|
||||
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 {
|
||||
isValid: false,
|
||||
parsed: parsedChartData,
|
||||
transformed: parsed,
|
||||
parsed: [],
|
||||
transformed: transformed,
|
||||
message: validationMessage,
|
||||
};
|
||||
}
|
||||
return { isValid, parsed: parsedChartData, transformed: parsedChartData };
|
||||
|
||||
return { isValid, parsed, transformed };
|
||||
},
|
||||
[VALIDATION_TYPES.CUSTOM_FUSION_CHARTS_DATA]: (
|
||||
value: any,
|
||||
|
|
@ -419,6 +397,7 @@ export const VALIDATORS: Record<VALIDATION_TYPES, Validator> = {
|
|||
if (props.chartName && parsed.dataSource && parsed.dataSource.chart) {
|
||||
parsed.dataSource.chart.caption = props.chartName;
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
return {
|
||||
isValid,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user