diff --git a/app/client/cypress/fixtures/example.json b/app/client/cypress/fixtures/example.json index 176cec376a..3dee58e627 100644 --- a/app/client/cypress/fixtures/example.json +++ b/app/client/cypress/fixtures/example.json @@ -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, diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Chart_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Chart_spec.js index 5fa2fdb40b..7331a78fa1 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Chart_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Chart_spec.js @@ -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 diff --git a/app/client/cypress/manual_TestSuite/Org_Logo_Del.js b/app/client/cypress/manual_TestSuite/Org_Logo_Del.js new file mode 100644 index 0000000000..3ee6278b2b --- /dev/null +++ b/app/client/cypress/manual_TestSuite/Org_Logo_Del.js @@ -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 + }); +}); diff --git a/app/client/cypress/manual_TestSuite/Org_Logo_Set.js b/app/client/cypress/manual_TestSuite/Org_Logo_Set.js new file mode 100644 index 0000000000..fa99552481 --- /dev/null +++ b/app/client/cypress/manual_TestSuite/Org_Logo_Set.js @@ -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 + }); +}); diff --git a/app/client/cypress/manual_TestSuite/Share_User_Icon.js b/app/client/cypress/manual_TestSuite/Share_User_Icon.js new file mode 100644 index 0000000000..c87386fc4e --- /dev/null +++ b/app/client/cypress/manual_TestSuite/Share_User_Icon.js @@ -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 + }); +}); diff --git a/app/client/cypress/manual_TestSuite/Table_Filter_Test_spec.js b/app/client/cypress/manual_TestSuite/Table_Filter_Test_spec.js new file mode 100644 index 0000000000..bd5fd602e7 --- /dev/null +++ b/app/client/cypress/manual_TestSuite/Table_Filter_Test_spec.js @@ -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 + }); +}); diff --git a/app/client/package.json b/app/client/package.json index c261842a74..c16508816f 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -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", diff --git a/app/client/src/actions/configsActions.tsx b/app/client/src/actions/configsActions.tsx deleted file mode 100644 index 0a97bf03a4..0000000000 --- a/app/client/src/actions/configsActions.tsx +++ /dev/null @@ -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, - }; -}; diff --git a/app/client/src/assets/icons/control/back.svg b/app/client/src/assets/icons/control/back.svg index b34278d7d9..3c600a22a9 100644 --- a/app/client/src/assets/icons/control/back.svg +++ b/app/client/src/assets/icons/control/back.svg @@ -1 +1,4 @@ - \ No newline at end of file + + + + diff --git a/app/client/src/components/designSystems/appsmith/ChartComponent.tsx b/app/client/src/components/designSystems/appsmith/ChartComponent.tsx index 5b102f03e3..faccb28c53 100644 --- a/app/client/src/components/designSystems/appsmith/ChartComponent.tsx +++ b/app/client/src/components/designSystems/appsmith/ChartComponent.tsx @@ -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 = { + 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 { chartInstance = new FusionCharts(); + getChartType = () => { const { chartType, allowHorizontalScroll, chartData } = this.props; const isMSChart = chartData.length > 1; @@ -216,6 +240,23 @@ class ChartComponent extends React.Component { } }; + 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 { }; 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 { /* 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 { 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 ( diff --git a/app/client/src/components/propertyControls/CustomFusionChartControl.tsx b/app/client/src/components/propertyControls/CustomFusionChartControl.tsx new file mode 100644 index 0000000000..20d22cf008 --- /dev/null +++ b/app/client/src/components/propertyControls/CustomFusionChartControl.tsx @@ -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 ( + + ); + } + static getControlType() { + return "CUSTOM_FUSION_CHARTS_DATA"; + } +} + +export default CustomFusionChartControl; diff --git a/app/client/src/components/propertyControls/index.ts b/app/client/src/components/propertyControls/index.ts index a92d2ee16e..8b89e875cd 100644 --- a/app/client/src/components/propertyControls/index.ts +++ b/app/client/src/components/propertyControls/index.ts @@ -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, diff --git a/app/client/src/constants/FieldExpectedValue.ts b/app/client/src/constants/FieldExpectedValue.ts index 936069c0e0..530b8dcc14 100644 --- a/app/client/src/constants/FieldExpectedValue.ts +++ b/app/client/src/constants/FieldExpectedValue.ts @@ -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", diff --git a/app/client/src/constants/WidgetValidation.ts b/app/client/src/constants/WidgetValidation.ts index e49629bd48..23eb649159 100644 --- a/app/client/src/constants/WidgetValidation.ts +++ b/app/client/src/constants/WidgetValidation.ts @@ -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", diff --git a/app/client/src/pages/Editor/PropertyPane/PropertySection.tsx b/app/client/src/pages/Editor/PropertyPane/PropertySection.tsx index 5808825c1c..f0ccd8d313 100644 --- a/app/client/src/pages/Editor/PropertyPane/PropertySection.tsx +++ b/app/client/src/pages/Editor/PropertyPane/PropertySection.tsx @@ -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; } } diff --git a/app/client/src/sagas/InitSagas.ts b/app/client/src/sagas/InitSagas.ts index b9e42ad991..4e8c29572a 100644 --- a/app/client/src/sagas/InitSagas.ts +++ b/app/client/src/sagas/InitSagas.ts @@ -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)), diff --git a/app/client/src/selectors/propertyPaneSelectors.tsx b/app/client/src/selectors/propertyPaneSelectors.tsx index d74d1b30a5..c9a3cf955a 100644 --- a/app/client/src/selectors/propertyPaneSelectors.tsx +++ b/app/client/src/selectors/propertyPaneSelectors.tsx @@ -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"; diff --git a/app/client/src/utils/PropertyControlFactory.tsx b/app/client/src/utils/PropertyControlFactory.tsx index f997c8e5eb..e94ef1c0cc 100644 --- a/app/client/src/utils/PropertyControlFactory.tsx +++ b/app/client/src/utils/PropertyControlFactory.tsx @@ -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, diff --git a/app/client/src/widgets/BaseWidget.tsx b/app/client/src/widgets/BaseWidget.tsx index 3c4dba6207..f917f5a5a1 100644 --- a/app/client/src/widgets/BaseWidget.tsx +++ b/app/client/src/widgets/BaseWidget.tsx @@ -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 { 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 { diff --git a/app/client/src/widgets/ChartWidget/index.tsx b/app/client/src/widgets/ChartWidget/index.tsx index e60f8f357f..156b15f4cb 100644 --- a/app/client/src/widgets/ChartWidget/index.tsx +++ b/app/client/src/widgets/ChartWidget/index.tsx @@ -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 { 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 { 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; diff --git a/app/client/src/widgets/ChartWidget/propertyConfig.test.ts b/app/client/src/widgets/ChartWidget/propertyConfig.test.ts new file mode 100644 index 0000000000..41da9f158d --- /dev/null +++ b/app/client/src/widgets/ChartWidget/propertyConfig.test.ts @@ -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 { + toBePropertyPaneConfig(): R; + } + } +} +const validateControl = (control: Record) => { + 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) => { + 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>).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(); + }); + }); +}); diff --git a/app/client/src/widgets/ChartWidget/propertyConfig.ts b/app/client/src/widgets/ChartWidget/propertyConfig.ts index 2964491ea3..e2e47d5dbc 100644 --- a/app/client/src/widgets/ChartWidget/propertyConfig.ts +++ b/app/client/src/widgets/ChartWidget/propertyConfig.ts @@ -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", }, ], }, diff --git a/app/client/src/widgets/FormWidget.tsx b/app/client/src/widgets/FormWidget.tsx index 7a9574f14e..80f3889d1b 100644 --- a/app/client/src/widgets/FormWidget.tsx +++ b/app/client/src/widgets/FormWidget.tsx @@ -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) { diff --git a/app/client/src/workers/validations.test.ts b/app/client/src/workers/validations.test.ts index 2d75c87e69..4f06c62768 100644 --- a/app/client/src/workers/validations.test.ts +++ b/app/client/src/workers/validations.test.ts @@ -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 = [ diff --git a/app/client/src/workers/validations.ts b/app/client/src/workers/validations.ts index 83e7357a9d..b305216f66 100644 --- a/app/client/src/workers/validations.ts +++ b/app/client/src/workers/validations.ts @@ -365,6 +365,39 @@ export const VALIDATORS: Record = { } 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,