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:
parent
4a05b5d320
commit
1ccece69e1
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
14
app/client/cypress/manual_TestSuite/Org_Logo_Del.js
Normal file
14
app/client/cypress/manual_TestSuite/Org_Logo_Del.js
Normal 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
|
||||
});
|
||||
});
|
||||
14
app/client/cypress/manual_TestSuite/Org_Logo_Set.js
Normal file
14
app/client/cypress/manual_TestSuite/Org_Logo_Set.js
Normal 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
|
||||
});
|
||||
});
|
||||
13
app/client/cypress/manual_TestSuite/Share_User_Icon.js
Normal file
13
app/client/cypress/manual_TestSuite/Share_User_Icon.js
Normal 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
|
||||
});
|
||||
});
|
||||
|
|
@ -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
|
||||
});
|
||||
});
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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 |
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
84
app/client/src/widgets/ChartWidget/propertyConfig.test.ts
Normal file
84
app/client/src/widgets/ChartWidget/propertyConfig.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user