Custom FusionCharts Config (#2670)

* Property pane enhancements

- Property pane sections are collapsible
- Property pane controls can be hidden conditionally
- Property pane configurations now come from the widget instead of a global config file
- Property pane property updates can be hooked with other related updates
- Property pane control and section ids are generated dynamically now.

* Add chart type: "Custom FusionChart" (#2996)

Co-authored-by: Zeger Hoogeboom <zegerhoogeboom@users.noreply.github.com>
Co-authored-by: zeger <zeger@equinoxai.com>
This commit is contained in:
Abhinav Jha 2021-03-25 03:35:04 +05:30 committed by GitHub
parent 4a05b5d320
commit 1ccece69e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 734 additions and 37 deletions

View File

@ -196,6 +196,50 @@
"orderAmount": 9.99
}
],
"ChartCustomConfig": {"type": "area2d",
"dataSource": {
"chart": {
"caption": "Countries With Most Oil Reserves [2017-18]",
"subCaption": "In MMbbl = One Million barrels",
"xAxisName": "Country",
"yAxisName": "Reserves (MMbbl)",
"numberSuffix": "K"
},
"data": [
{
"label": "Venezuela",
"value": "290"
},
{
"label": "Saudi",
"value": "260"
},
{
"label": "Canada",
"value": "180"
},
{
"label": "Iran",
"value": "140"
},
{
"label": "Russia",
"value": "115"
},
{
"label": "UAE",
"value": "100"
},
{
"label": "US",
"value": "30"
},
{
"label": "China",
"value": "30"
}
]
}},
"TableInputWithNull": [
{
"id": 2381224,

View File

@ -67,9 +67,11 @@ describe("Chart Widget Functionality", function() {
.click({ force: true })
.type(this.data.command)
.type(this.data.ylabel);
//Close edit prop
cy.get(commonlocators.editPropCrossButton).click();
});
it("Chart Widget Functionality To Unchecked Visible Widget", function() {
cy.togglebarDisable(commonlocators.visibleCheckbox);
cy.PublishtheApp();
@ -96,6 +98,34 @@ describe("Chart Widget Functionality", function() {
.should("exist");
cy.get(publish.backToEditor).click();
});
it("Chart Widget Custom Config Feature", function() {
// Note: This only checks for crashes in custom config
cy.get(viewWidgetsPage.chartType)
.last()
.click({ force: true });
cy.get(commonlocators.dropdownmenu)
.children()
.contains("Custom Chart")
.click();
cy.get(viewWidgetsPage.chartType)
.last()
.should("have.text", "Custom Chart");
cy.testJsontext(
"customfusionchartconfiguration",
`{{${JSON.stringify(this.data.ChartCustomConfig)}}}`,
);
cy.get(viewWidgetsPage.chartWidget)
.should("be.visible")
.and((chart) => {
expect(chart.height()).to.be.greaterThan(200);
});
cy.get(viewWidgetsPage.chartWidget).should("have.css", "opacity", "1");
//Close edit prop
cy.get(commonlocators.editPropCrossButton).click();
});
});
afterEach(() => {
// put your clean up code if any

View File

@ -0,0 +1,14 @@
const homePage = require("../../../locators/HomePage.json");
describe("Deletion of organisational Logo ", function() {
it(" org logo upload ", function() {
//Click on the dropdown next to organisational Name
// Navigate between tabs
// Naviagte to General Tab
// Add an Organisational Logo
// Wait until it loads
// Switch between Tabs
// Click on the remove Icon
//Ensure the organisational Logo is deleted
});
});

View File

@ -0,0 +1,14 @@
const homePage = require("../../../locators/HomePage.json");
describe("insert organisational Logo ", function() {
it(" org logo upload ", function() {
//Click on the dropdown next to organisational Name
// Navigate between tabs
// Naviagte to General Tab
// Add an Organisational Logo
//Wait until it loads
// Switch between Tabs
// Navigate to General Tab and ensure the logo exsits
//navigate back to Homepage
});
});

View File

@ -0,0 +1,13 @@
const homePage = require("../../../locators/HomePage.json");
describe("Shared user icon ", function() {
it(" User Icon is disaplyed to user ", function() {
// Navigate to home Page
//Click on Share Icon
// Click on Field to add an Email Id
// Click on the Roles field
// Add an role from the Dropdown
// CLick on Invite
//Now observe the icon next to the Share Icon
});
});

View File

@ -0,0 +1,11 @@
const dsl = require("../../../fixtures/tableWidgetDsl.json");
describe("Test for Table Filter ", function() {
it("Table Filter", function() {
//Add a table
// click on the column action item
// Click on Select a datatype
// Click on Filter option
// ensure to add filter
});
});

View File

@ -14,6 +14,7 @@
"@blueprintjs/select": "^3.10.0",
"@blueprintjs/timezone": "^3.6.0",
"@craco/craco": "^5.7.0",
"@fusioncharts/powercharts": "^3.16.0",
"@github/g-emoji-element": "^1.1.5",
"@manaflair/redux-batch": "^1.0.0",
"@optimizely/optimizely-sdk": "^4.0.0",

View File

@ -1,12 +0,0 @@
import { ReduxActionTypes } from "constants/ReduxActionConstants";
export type EditorConfigIdsType = {
widgetCardsPaneId?: string;
widgetConfigsId?: string;
};
export const fetchEditorConfigs = () => {
return {
type: ReduxActionTypes.FETCH_CONFIGS_INIT,
};
};

View File

@ -1 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16"><path fill="#A2A6A8" d="M13.3327 7.33341H5.21935L8.94601 3.60675L7.99935 2.66675L2.66602 8.00008L7.99935 13.3334L8.93935 12.3934L5.21935 8.66675H13.3327V7.33341Z"/></svg>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.3327 7.33341H5.21935L8.94601 3.60675L7.99935 2.66675L2.66602 8.00008L7.99935 13.3334L8.93935 12.3934L5.21935 8.66675H13.3327V7.33341Z" fill="#A2A6A8"/>
</svg>

Before

Width:  |  Height:  |  Size: 265 B

After

Width:  |  Height:  |  Size: 269 B

View File

@ -4,16 +4,38 @@ import styled from "styled-components";
import { getBorderCSSShorthand, invisible } from "constants/DefaultTheme";
import { getAppsmithConfigs } from "configs";
import { ChartType, ChartData, ChartDataPoint } from "widgets/ChartWidget";
import { ChartData, ChartDataPoint, ChartType } from "widgets/ChartWidget";
import log from "loglevel";
export interface CustomFusionChartConfig {
type: string;
dataSource?: any;
}
const FusionCharts = require("fusioncharts");
const Charts = require("fusioncharts/fusioncharts.charts");
const FusionTheme = require("fusioncharts/themes/fusioncharts.theme.fusion");
const plugins: Record<string, any> = {
Charts: require("fusioncharts/fusioncharts.charts"),
FusionTheme: require("fusioncharts/themes/fusioncharts.theme.fusion"),
Widgets: require("fusioncharts/fusioncharts.widgets"),
ZoomScatter: require("fusioncharts/fusioncharts.zoomscatter"),
ZoomLine: require("fusioncharts/fusioncharts.zoomline"),
PowerCharts: require("fusioncharts/fusioncharts.powercharts"),
TimeSeries: require("fusioncharts/fusioncharts.timeseries"),
OverlappedColumn: require("fusioncharts/fusioncharts.overlappedcolumn2d"),
OverlappedBar: require("fusioncharts/fusioncharts.overlappedbar2d"),
TreeMap: require("fusioncharts/fusioncharts.treemap"),
Maps: require("fusioncharts/fusioncharts.maps"),
Gantt: require("fusioncharts/fusioncharts.gantt"),
VML: require("fusioncharts/fusioncharts.vml"),
};
// Enable all plugins.
// This is needed to support custom chart configs
Object.keys(plugins).forEach((key: string) =>
(plugins[key] as any)(FusionCharts),
);
const { fusioncharts } = getAppsmithConfigs();
Charts(FusionCharts);
FusionTheme(FusionCharts);
FusionCharts.options.license({
key: fusioncharts.licenseKey,
creditLabel: false,
@ -22,6 +44,7 @@ FusionCharts.options.license({
export interface ChartComponentProps {
chartType: ChartType;
chartData: ChartData[];
customFusionChartConfig: CustomFusionChartConfig;
xAxisName: string;
yAxisName: string;
chartName: string;
@ -47,6 +70,7 @@ const CanvasContainer = styled.div<
class ChartComponent extends React.Component<ChartComponentProps> {
chartInstance = new FusionCharts();
getChartType = () => {
const { chartType, allowHorizontalScroll, chartData } = this.props;
const isMSChart = chartData.length > 1;
@ -216,6 +240,23 @@ class ChartComponent extends React.Component<ChartComponentProps> {
}
};
getCustomFusionChartDataSource = () => {
let config = this.props.customFusionChartConfig as CustomFusionChartConfig;
if (config && config.dataSource) {
config = {
...config,
dataSource: {
...config.dataSource,
chart: {
...config.dataSource.chart,
caption: this.props.chartName || config.dataSource.chart.caption,
},
},
};
}
return config;
};
getScrollChartDataSource = () => {
const chartConfig = this.getChartConfig();
@ -238,6 +279,16 @@ class ChartComponent extends React.Component<ChartComponentProps> {
};
createGraph = () => {
if (this.props.chartType === "CUSTOM_FUSION_CHART") {
const chartConfig = {
renderAt: this.props.widgetId + "chart-container",
width: "100%",
height: "100%",
...this.getCustomFusionChartDataSource(),
};
this.chartInstance = new FusionCharts(chartConfig);
return;
}
const dataSource =
this.props.allowHorizontalScroll && this.props.chartType !== "PIE_CHART"
? this.getScrollChartDataSource()
@ -270,7 +321,11 @@ class ChartComponent extends React.Component<ChartComponentProps> {
/* Component could be unmounted before FusionCharts is ready,
this check ensure we don't render on unmounted component */
if (this.chartInstance) {
this.chartInstance.render();
try {
this.chartInstance.render();
} catch (e) {
log.error(e);
}
}
});
}
@ -283,6 +338,17 @@ class ChartComponent extends React.Component<ChartComponentProps> {
componentDidUpdate(prevProps: ChartComponentProps) {
if (!_.isEqual(prevProps, this.props)) {
if (this.props.chartType === "CUSTOM_FUSION_CHART") {
const chartConfig = {
renderAt: this.props.widgetId + "chart-container",
width: "100%",
height: "100%",
...this.getCustomFusionChartDataSource(),
};
this.chartInstance = new FusionCharts(chartConfig);
this.chartInstance.render();
return;
}
const chartType = this.getChartType();
this.chartInstance.chartType(chartType);
if (

View File

@ -0,0 +1,33 @@
import React from "react";
import InputTextControl, { InputText } from "./InputTextControl";
class CustomFusionChartControl extends InputTextControl {
render() {
const expected = "{\n type: string,\n dataSource: Object\n}";
const {
propertyValue,
isValid,
label,
placeholderText,
dataTreePath,
validationMessage,
} = this.props;
return (
<InputText
label={label}
value={propertyValue}
onChange={this.onTextChange}
isValid={isValid}
errorMessage={validationMessage}
expected={expected}
placeholder={placeholderText}
dataTreePath={dataTreePath}
/>
);
}
static getControlType() {
return "CUSTOM_FUSION_CHARTS_DATA";
}
}
export default CustomFusionChartControl;

View File

@ -42,6 +42,7 @@ import ButtonTabControl, {
import MultiSwitchControl, {
MultiSwitchControlProps,
} from "components/propertyControls/MultiSwitchControl";
import CustomFusionChartControl from "./CustomFusionChartControl";
export const PropertyControls = {
InputTextControl,
@ -55,6 +56,7 @@ export const PropertyControls = {
ColumnActionSelectorControl,
MultiSwitchControl,
ChartDataControl,
CustomFusionChartControl,
LocationSearchControl,
StepControl,
TabControl,

View File

@ -68,7 +68,8 @@ const FIELD_VALUES: Record<
},
CHART_WIDGET: {
chartName: "string",
chartType: "LINE_CHART | BAR_CHART | PIE_CHART | COLUMN_CHART | AREA_CHART",
chartType:
"LINE_CHART | BAR_CHART | PIE_CHART | COLUMN_CHART | AREA_CHART | CUSTOM_FUSION_CHART",
xAxisName: "string",
yAxisName: "string",
isVisible: "boolean",

View File

@ -18,6 +18,7 @@ export const VALIDATION_TYPES = {
MAX_DATE: "MAX_DATE",
TABS_DATA: "TABS_DATA",
CHART_DATA: "CHART_DATA",
CUSTOM_FUSION_CHARTS_DATA: "CUSTOM_FUSION_CHARTS_DATA",
MARKERS: "MARKERS",
ACTION_SELECTOR: "ACTION_SELECTOR",
ARRAY_ACTION_SELECTOR: "ARRAY_ACTION_SELECTOR",

View File

@ -55,8 +55,8 @@ const areEqual = (prev: PropertySectionProps, next: PropertySectionProps) => {
export const PropertySection = memo((props: PropertySectionProps) => {
const [isOpen, open] = useState(!!props.isDefaultOpen);
const widgetProps: any = useSelector(getWidgetPropsForPropertyPane);
if (props.hidden && props.propertyPath) {
if (props.propertyPath && props.hidden(widgetProps, props.propertyPath)) {
if (props.hidden) {
if (props.hidden(widgetProps, props.propertyPath || "")) {
return null;
}
}

View File

@ -16,7 +16,6 @@ import {
} from "constants/ReduxActionConstants";
import { ERROR_CODES } from "constants/ApiConstants";
import { fetchEditorConfigs } from "actions/configsActions";
import {
fetchPage,
fetchPageList,
@ -52,7 +51,6 @@ function* initializeEditorSaga(
yield put({ type: ReduxActionTypes.START_EVALUATION });
yield all([
put(fetchPageList(applicationId, APP_MODE.EDIT)),
put(fetchEditorConfigs()),
put(fetchActions(applicationId)),
put(fetchPage(pageId)),
put(fetchApplication(applicationId, APP_MODE.EDIT)),

View File

@ -1,7 +1,6 @@
import { createSelector } from "reselect";
import { AppState } from "reducers";
import { PropertyPaneReduxState } from "reducers/uiReducers/propertyPaneReducer";
import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer";
import { WidgetProps } from "widgets/BaseWidget";
import { DataTree, DataTreeWidget } from "entities/DataTree/dataTreeFactory";

View File

@ -28,7 +28,6 @@ class PropertyControlFactory {
if (customEditor) controlBuilder = this.controlMap.get(customEditor);
else controlBuilder = this.controlMap.get("CODE_EDITOR");
}
if (controlBuilder) {
const controlProps: ControlProps = {
...controlData,

View File

@ -33,6 +33,7 @@ import {
WidgetDynamicPathListProps,
WidgetEvaluatedProps,
} from "../utils/DynamicBindingUtils";
import { PropertyPaneConfig } from "constants/PropertyControlConstants";
import { BatchPropertyUpdatePayload } from "actions/controlActions";
/***
@ -53,6 +54,9 @@ abstract class BaseWidget<
> extends Component<T, K> {
static contextType = EditorContext;
static getPropertyPaneConfig(): PropertyPaneConfig[] {
return [];
}
// Needed to send a default no validation option. In case a widget needs
// validation implement this in the widget class again
static getPropertyValidationMap(): WidgetPropertyValidationType {

View File

@ -9,6 +9,7 @@ import { retryPromise } from "utils/AppsmithUtils";
import { EventType } from "constants/ActionConstants";
import withMeta, { WithMeta } from "widgets/MetaHOC";
import propertyConfig from "widgets/ChartWidget/propertyConfig";
import { CustomFusionChartConfig } from "components/designSystems/appsmith/ChartComponent";
const ChartComponent = lazy(() =>
retryPromise(() =>
@ -26,6 +27,7 @@ class ChartWidget extends BaseWidget<ChartWidgetProps, WidgetState> {
chartName: VALIDATION_TYPES.TEXT,
isVisible: VALIDATION_TYPES.BOOLEAN,
chartData: VALIDATION_TYPES.CHART_DATA,
customFusionChartConfig: VALIDATION_TYPES.CUSTOM_FUSION_CHARTS_DATA,
};
}
@ -63,6 +65,7 @@ class ChartWidget extends BaseWidget<ChartWidgetProps, WidgetState> {
yAxisName={this.props.yAxisName}
chartName={this.props.chartName}
chartData={this.props.chartData}
customFusionChartConfig={this.props.customFusionChartConfig}
widgetId={this.props.widgetId}
onDataPointClick={this.onDataPointClick}
allowHorizontalScroll={this.props.allowHorizontalScroll}
@ -82,7 +85,8 @@ export type ChartType =
| "PIE_CHART"
| "COLUMN_CHART"
| "AREA_CHART"
| "SCATTER_CHART";
| "SCATTER_CHART"
| "CUSTOM_FUSION_CHART";
export interface ChartDataPoint {
x: any;
@ -97,6 +101,7 @@ export interface ChartData {
export interface ChartWidgetProps extends WidgetProps, WithMeta {
chartType: ChartType;
chartData: ChartData[];
customFusionChartConfig: { config: CustomFusionChartConfig };
xAxisName: string;
yAxisName: string;
chartName: string;

View File

@ -0,0 +1,84 @@
/* eslint-disable @typescript-eslint/no-namespace */
import { isString, get } from "lodash";
import config from "./propertyConfig";
declare global {
namespace jest {
interface Matchers<R> {
toBePropertyPaneConfig(): R;
}
}
}
const validateControl = (control: Record<string, unknown>) => {
if (typeof control !== "object") return false;
const properties = [
"propertyName",
"controlType",
"isBindProperty",
"isTriggerProperty",
];
properties.forEach((prop: string) => {
if (!control.hasOwnProperty(prop)) {
return false;
}
const value = control[prop];
if (isString(value) && value.length === 0) return false;
});
return true;
};
const validateSection = (section: Record<string, unknown>) => {
if (typeof section !== "object") return false;
if (!section.hasOwnProperty("sectionName")) return false;
const name = section.sectionName;
if ((name as string).length === 0) return false;
if (section.children) {
return (section.children as Array<Record<string, unknown>>).forEach(
(child) => {
if (!validateControl(child)) return false;
},
);
}
return true;
};
expect.extend({
toBePropertyPaneConfig(received) {
if (Array.isArray(received)) {
let pass = true;
received.forEach((section) => {
if (!validateSection(section) && !validateControl(section))
pass = false;
});
return {
pass,
message: () => "Expected value to be a property pane config internal",
};
}
return {
pass: false,
message: () => "Expected value to be a property pane config external",
};
},
});
describe("Validate Chart Widget's property config", () => {
it("Validates Chart Widget's property config", () => {
expect(config).toBePropertyPaneConfig();
});
it("Validates config when chartType is CUSTOM_FUSION_CHART", () => {
const hiddenFn: (props: any) => boolean = get(config, "[1].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")];
hiddenFns.forEach((fn: (props: any) => boolean) => {
const result = fn({ chartType: "CUSTOM_FUSION_CHART" });
expect(result).toBeTruthy();
});
});
});

View File

@ -1,3 +1,5 @@
import { ChartWidgetProps } from "widgets/ChartWidget";
export default [
{
sectionName: "General",
@ -37,15 +39,41 @@ export default [
label: "Area Chart",
value: "AREA_CHART",
},
{
label: "Custom Chart",
value: "CUSTOM_FUSION_CHART",
},
],
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
},
{
propertyName: "isVisible",
label: "Visible",
helpText: "Controls the visibility of the widget",
controlType: "SWITCH",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
},
],
},
{
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",
},
{
sectionName: "Chart Data",
hidden: (props: ChartWidgetProps) =>
props.chartType === "CUSTOM_FUSION_CHART",
children: [
{
helpText: "Populates the chart with the data",
@ -53,6 +81,7 @@ export default [
placeholderText: 'Enter [{ "x": "val", "y": "val" }]',
label: "Chart Series",
controlType: "CHART_DATA",
isBindProperty: false,
isTriggerProperty: false,
children: [
@ -78,6 +107,8 @@ export default [
},
{
sectionName: "Axis",
hidden: (props: ChartWidgetProps) =>
props.chartType === "CUSTOM_FUSION_CHART",
children: [
{
helpText: "Specifies the label of the x-axis",
@ -104,15 +135,7 @@ export default [
controlType: "SWITCH",
isBindProperty: false,
isTriggerProperty: false,
},
{
propertyName: "isVisible",
label: "Visible",
helpText: "Controls the visibility of the widget",
controlType: "SWITCH",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
hidden: (x: any) => x.chartType === "CUSTOM_FUSION_CHART",
},
],
},

View File

@ -8,6 +8,40 @@ import * as Sentry from "@sentry/react";
import withMeta from "./MetaHOC";
class FormWidget extends ContainerWidget {
static getPropertyPaneConfig() {
return [
{
sectionName: "General",
children: [
{
propertyName: "backgroundColor",
label: "Background Color",
helpText: "Use a html color name, HEX, RGB or RGBA value",
placeholderText: "#FFFFFF / Gray / rgb(255, 99, 71)",
controlType: "INPUT_TEXT",
isBindProperty: true,
isTriggerProperty: false,
},
{
helpText: "Controls the visibility of the widget",
propertyName: "isVisible",
label: "Visible",
controlType: "SWITCH",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
},
{
propertyName: "shouldScrollContents",
label: "Scroll Contents",
controlType: "SWITCH",
isBindProperty: false,
isTriggerProperty: false,
},
],
},
];
}
checkInvalidChildren = (children: WidgetProps[]): boolean => {
return _.some(children, (child) => {
if ("children" in child) {

View File

@ -164,6 +164,303 @@ describe("Validate Validators", () => {
});
});
describe("Chart Custom Config validator", () => {
const validator = VALIDATORS.CUSTOM_FUSION_CHARTS_DATA;
it("correctly validates ", () => {
const cases = [
{
input: {
type: "area2d",
dataSource: {
chart: {
caption: "Countries With Most Oil Reserves [2017-18]",
subCaption: "In MMbbl = One Million barrels",
xAxisName: "Country",
yAxisName: "Reserves (MMbbl)",
numberSuffix: "K",
},
data: [
{
label: "Venezuela",
value: "290",
},
{
label: "Saudi",
value: "260",
},
{
label: "Canada",
value: "180",
},
{
label: "Iran",
value: "140",
},
{
label: "Russia",
value: "115",
},
{
label: "UAE",
value: "100",
},
{
label: "US",
value: "30",
},
{
label: "China",
value: "30",
},
],
},
},
output: {
isValid: true,
parsed: {
type: "area2d",
dataSource: {
chart: {
caption: "Countries With Most Oil Reserves [2017-18]",
subCaption: "In MMbbl = One Million barrels",
xAxisName: "Country",
yAxisName: "Reserves (MMbbl)",
numberSuffix: "K",
},
data: [
{
label: "Venezuela",
value: "290",
},
{
label: "Saudi",
value: "260",
},
{
label: "Canada",
value: "180",
},
{
label: "Iran",
value: "140",
},
{
label: "Russia",
value: "115",
},
{
label: "UAE",
value: "100",
},
{
label: "US",
value: "30",
},
{
label: "China",
value: "30",
},
],
},
},
transformed: {
type: "area2d",
dataSource: {
chart: {
caption: "Countries With Most Oil Reserves [2017-18]",
subCaption: "In MMbbl = One Million barrels",
xAxisName: "Country",
yAxisName: "Reserves (MMbbl)",
numberSuffix: "K",
},
data: [
{
label: "Venezuela",
value: "290",
},
{
label: "Saudi",
value: "260",
},
{
label: "Canada",
value: "180",
},
{
label: "Iran",
value: "140",
},
{
label: "Russia",
value: "115",
},
{
label: "UAE",
value: "100",
},
{
label: "US",
value: "30",
},
{
label: "China",
value: "30",
},
],
},
},
},
},
{
input: {
type: "area2d",
dataSource: {
data: [
{
label: "Venezuela",
value: "290",
},
{
label: "Saudi",
value: "260",
},
{
label: "Canada",
value: "180",
},
{
label: "Iran",
value: "140",
},
{
label: "Russia",
value: "115",
},
{
label: "UAE",
value: "100",
},
{
label: "US",
value: "30",
},
{
label: "China",
value: "30",
},
],
},
},
output: {
isValid: true,
parsed: {
type: "area2d",
dataSource: {
data: [
{
label: "Venezuela",
value: "290",
},
{
label: "Saudi",
value: "260",
},
{
label: "Canada",
value: "180",
},
{
label: "Iran",
value: "140",
},
{
label: "Russia",
value: "115",
},
{
label: "UAE",
value: "100",
},
{
label: "US",
value: "30",
},
{
label: "China",
value: "30",
},
],
},
},
transformed: {
type: "area2d",
dataSource: {
data: [
{
label: "Venezuela",
value: "290",
},
{
label: "Saudi",
value: "260",
},
{
label: "Canada",
value: "180",
},
{
label: "Iran",
value: "140",
},
{
label: "Russia",
value: "115",
},
{
label: "UAE",
value: "100",
},
{
label: "US",
value: "30",
},
{
label: "China",
value: "30",
},
],
},
},
},
},
{
input: {
type: undefined,
dataSource: undefined,
},
output: {
isValid: false,
message:
"Value does not match type: {type: string, dataSource: { chart: object, data: Array<{label: string, value: number}>}}",
parsed: {
type: undefined,
dataSource: undefined,
},
transformed: {
type: undefined,
dataSource: undefined,
},
},
},
];
for (const testCase of cases) {
const response = validator(testCase.input, DUMMY_WIDGET, {});
expect(response).toStrictEqual(testCase.output);
}
});
});
describe("validateDateString test", () => {
it("Check whether the valid date strings are recognized as valid", () => {
const validDateStrings = [

View File

@ -365,6 +365,39 @@ export const VALIDATORS: Record<ValidationType, Validator> = {
}
return { isValid, parsed: parsedChartData, transformed: parsedChartData };
},
[VALIDATION_TYPES.CUSTOM_FUSION_CHARTS_DATA]: (
value: any,
props: WidgetProps,
dataTree?: DataTree,
): ValidationResponse => {
const { isValid, parsed } = VALIDATORS[VALIDATION_TYPES.OBJECT](
value,
props,
dataTree,
);
if (props.chartName && parsed.dataSource && parsed.dataSource.chart) {
parsed.dataSource.chart.caption = props.chartName;
}
if (!isValid) {
return {
isValid,
parsed,
message: `${WIDGET_TYPE_VALIDATION_ERROR}: {type: string, dataSource: { chart: object, data: Array<{label: string, value: number}>}}`,
};
}
if (parsed.renderAt) {
delete parsed.renderAt;
}
if (!parsed.dataSource || !parsed.type) {
return {
isValid: false,
parsed: parsed,
transformed: parsed,
message: `${WIDGET_TYPE_VALIDATION_ERROR}: {type: string, dataSource: { chart: object, data: Array<{label: string, value: number}>}}`,
};
}
return { isValid, parsed, transformed: parsed };
},
[VALIDATION_TYPES.MARKERS]: (
value: any,
props: WidgetProps,