diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/LayoutWidgets/Container_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/LayoutWidgets/Container_spec.js index 1729993c2d..883257100f 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/LayoutWidgets/Container_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/LayoutWidgets/Container_spec.js @@ -22,10 +22,13 @@ describe("Container Widget Functionality", function() { /** * @param{Text} Random Colour */ - cy.testCodeMirror(this.data.colour); + cy.get(widgetsPage.backgroundcolorPicker) + .first() + .click({ force: true }); + cy.xpath(widgetsPage.greenColor).click(); cy.get(widgetsPage.containerD) .should("have.css", "background-color") - .and("eq", this.data.rgbValue); + .and("eq", "rgb(3, 179, 101)"); /** * @param{toggleButton Css} Assert to be checked */ @@ -41,7 +44,7 @@ describe("Container Widget Functionality", function() { cy.get(widgetsPage.containerD) .eq(0) .should("have.css", "background-color") - .and("eq", this.data.rgbValue); + .and("eq", "rgb(3, 179, 101)"); }); afterEach(() => { // put your clean up code if any diff --git a/app/client/cypress/locators/Widgets.json b/app/client/cypress/locators/Widgets.json index fda4bfc2c6..3e67eecebb 100644 --- a/app/client/cypress/locators/Widgets.json +++ b/app/client/cypress/locators/Widgets.json @@ -65,6 +65,7 @@ "verticalCenter": ".t--icon-tab-CENTER", "verticalBottom": ".t--icon-tab-BOTTOM", "textColor": ".t--property-control-textcolor input", + "backgroundcolorPicker": ".t--property-control-backgroundcolor input", "greenColor": "//div[@color='rgb(3, 179, 101)']", "toggleJsColor": ".t--property-control-textcolor .t--js-toggle", "backgroundColor": ".t--property-control-cellbackground input", diff --git a/app/client/jest.config.js b/app/client/jest.config.js index 43e381ac5f..6599de0964 100644 --- a/app/client/jest.config.js +++ b/app/client/jest.config.js @@ -15,7 +15,7 @@ module.exports = { moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node", "css"], moduleDirectories: ["node_modules", "src", "test"], transformIgnorePatterns: [ - "/node_modules/(?!codemirror|react-dnd|dnd-core|@babel|(@blueprintjs/core/lib/esnext)|(@blueprintjs/core/lib/esm)|@github)", + "/node_modules/(?!codemirror|react-dnd|dnd-core|@babel|(@blueprintjs/core/lib/esnext)|(@blueprintjs/core/lib/esm)|@github|lodash-es)", ], moduleNameMapper: { "\\.(css|less)$": "/test/__mocks__/styleMock.js", @@ -23,6 +23,7 @@ module.exports = { "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/test/__mocks__/fileMock.js", "^worker-loader!": "/test/__mocks__/workerMock.js", + "^!!raw-loader!": "/test/__mocks__/derivedMock.js", "test/(.*)": "/test/$1", }, globals: { diff --git a/app/client/package.json b/app/client/package.json index 858379e523..0e30ae3790 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -149,6 +149,7 @@ "start-prod": "REACT_APP_ENVIRONMENT=PRODUCTION craco start", "cytest": "REACT_APP_TESTING=TESTING REACT_APP_ENVIRONMENT=DEVELOPMENT craco start & ./node_modules/.bin/cypress open", "test:unit": "$(npm bin)/jest -b --colors --no-cache --coverage --collectCoverage=true --coverageDirectory='../../' --coverageReporters='json-summary'", + "test:jest": "$(npm bin)/jest --watch", "storybook": "start-storybook -p 9009 -s public", "build-storybook": "build-storybook -s public" }, @@ -212,6 +213,7 @@ "eslint-plugin-prettier": "^3.1.4", "eslint-plugin-react": "^7.21.3", "eslint-plugin-react-hooks": "^2.3.0", + "factory.ts": "^0.5.1", "jest-canvas-mock": "^2.3.1", "mocha": "^7.1.0", "mocha-junit-reporter": "^1.23.3", diff --git a/app/client/src/actions/pageActions.tsx b/app/client/src/actions/pageActions.tsx index e164e1ba9f..f454c0349d 100644 --- a/app/client/src/actions/pageActions.tsx +++ b/app/client/src/actions/pageActions.tsx @@ -227,12 +227,23 @@ export type WidgetAddChildren = { }>; }; +export type WidgetUpdateProperty = { + widgetId: string; + propertyPath: string; + propertyValue: any; +}; + export const updateWidget = ( operation: WidgetOperation, widgetId: string, payload: any, ): ReduxAction< - WidgetAddChild | WidgetMove | WidgetResize | WidgetDelete | WidgetAddChildren + | WidgetAddChild + | WidgetMove + | WidgetResize + | WidgetDelete + | WidgetAddChildren + | WidgetUpdateProperty > => { return { type: ReduxActionTypes["WIDGET_" + operation], diff --git a/app/client/src/components/designSystems/appsmith/TabsComponent.tsx b/app/client/src/components/designSystems/appsmith/TabsComponent.tsx index 820bd47509..3fc1e98d9c 100644 --- a/app/client/src/components/designSystems/appsmith/TabsComponent.tsx +++ b/app/client/src/components/designSystems/appsmith/TabsComponent.tsx @@ -1,7 +1,10 @@ import React, { RefObject, ReactNode, useEffect, useRef } from "react"; import styled, { css } from "styled-components"; import { ComponentProps } from "./BaseComponent"; -import { TabsWidgetProps, TabContainerWidgetProps } from "widgets/TabsWidget"; +import { + TabsWidgetProps, + TabContainerWidgetProps, +} from "widgets/Tabs/TabsWidget"; import { generateClassName, getCanvasClassName } from "utils/generators"; import { getBorderCSSShorthand } from "constants/DefaultTheme"; import ScrollIndicator from "components/ads/ScrollIndicator"; diff --git a/app/client/src/components/propertyControls/PrimaryColumnsControl.tsx b/app/client/src/components/propertyControls/PrimaryColumnsControl.tsx index 7d58cf71e6..93275856d6 100644 --- a/app/client/src/components/propertyControls/PrimaryColumnsControl.tsx +++ b/app/client/src/components/propertyControls/PrimaryColumnsControl.tsx @@ -257,7 +257,7 @@ class PrimaryColumnsControl extends BaseControl { this.props.openNextPanel({ ...originalColumn, - widgetId: this.props.widgetProperties.widgetId, + propPaneId: this.props.widgetProperties.widgetId, }); }; //Used to reorder columns diff --git a/app/client/src/components/propertyControls/TabControl.tsx b/app/client/src/components/propertyControls/TabControl.tsx index 4744ccd11a..cec947d058 100644 --- a/app/client/src/components/propertyControls/TabControl.tsx +++ b/app/client/src/components/propertyControls/TabControl.tsx @@ -1,12 +1,11 @@ -import React, { useState } from "react"; +import React, { useCallback } from "react"; import BaseControl, { ControlProps } from "./BaseControl"; import { - StyledHiddenIcon, StyledInputGroup, StyledPropertyPaneButton, - StyledVisibleIcon, StyledDragIcon, StyledDeleteIcon, + StyledEditIcon, } from "./StyledControls"; import styled from "constants/DefaultTheme"; import { generateReactKey } from "utils/generators"; @@ -63,12 +62,15 @@ type RenderComponentProps = { deleteOption: (index: number) => void; updateOption: (index: number, value: string) => void; toggleVisibility?: (index: number) => void; + onEdit?: (props: any) => void; }; function TabControlComponent(props: RenderComponentProps) { - const { deleteOption, updateOption, item, index, toggleVisibility } = props; + const { deleteOption, updateOption, item, index } = props; const debouncedUpdate = debounce(updateOption, 1000); - const [visibility, setVisibility] = useState(item.isVisible); + const handleChange = useCallback(() => props.onEdit && props.onEdit(index), [ + index, + ]); return ( @@ -89,29 +91,12 @@ function TabControlComponent(props: RenderComponentProps) { deleteOption(index); }} /> - {visibility || visibility === undefined ? ( - { - setVisibility(!visibility); - toggleVisibility && toggleVisibility(index); - }} - /> - ) : ( - { - setVisibility(!visibility); - toggleVisibility && toggleVisibility(index); - }} - /> - )} + ); } @@ -148,15 +133,37 @@ class TabControl extends BaseControl { } } - updateItems = (items: Array>) => { - this.updateProperty(this.props.propertyName, items); + updateItems = (items: Array>) => { + const tabsObj = items.reduce((obj: any, each: any, index: number) => { + obj[each.id] = { + ...each, + index, + }; + return obj; + }, {}); + this.updateProperty(this.props.propertyName, tabsObj); + }; + + onEdit = (index: number) => { + const tabs: Array<{ + id: string; + label: string; + }> = Object.values(this.props.propertyValue); + const tabToChange = tabs[index]; + this.props.openNextPanel({ + index, + ...tabToChange, + propPaneId: this.props.widgetProperties.widgetId, + }); }; render() { const tabs: Array<{ id: string; label: string; - }> = _.isString(this.props.propertyValue) ? [] : this.props.propertyValue; + }> = _.isString(this.props.propertyValue) + ? [] + : Object.values(this.props.propertyValue); return ( { updateOption={this.updateOption} updateItems={this.updateItems} toggleVisibility={this.toggleVisibility} + onEdit={this.onEdit} /> { }; deleteOption = (index: number) => { - let tabs: Array> = this.props.propertyValue.slice(); - if (tabs.length === 1) return; - delete tabs[index]; - tabs = tabs.filter(Boolean); - this.updateProperty(this.props.propertyName, tabs); + const tabsArray: any = Object.values(this.props.propertyValue); + const itemId = tabsArray[index].id; + if (tabsArray && tabsArray.length === 1) return; + const updatedArray = tabsArray.filter((eachItem: any, i: number) => { + return i !== index; + }); + const updatedObj = updatedArray.reduce( + (obj: any, each: any, index: number) => { + obj[each.id] = { + ...each, + index, + }; + return obj; + }, + {}, + ); + this.deleteProperties([`${this.props.propertyName}.${itemId}.isVisible`]); + this.updateProperty(this.props.propertyName, updatedObj); }; updateOption = (index: number, updatedLabel: string) => { - const tabs: Array<{ - id: string; - label: string; - }> = this.props.propertyValue; - const updatedTabs = tabs.map((tab, tabIndex) => { - if (index === tabIndex) { - return { - ...tab, - label: updatedLabel, - }; - } - return tab; - }); - this.updateProperty(this.props.propertyName, updatedTabs); + const tabsArray: any = Object.values(this.props.propertyValue); + const itemId = tabsArray[index].id; + this.updateProperty( + `${this.props.propertyName}.${itemId}.label`, + updatedLabel, + ); }; addOption = () => { - let tabs: Array<{ - id: string; - label: string; - widgetId: string; - isVisible: boolean; - }> = this.props.propertyValue; + let tabs = this.props.propertyValue; + const tabsArray = Object.values(tabs); const newTabId = generateReactKey({ prefix: "tab" }); const newTabLabel = getNextEntityName( "Tab ", - tabs.map((tab) => tab.label), + tabsArray.map((tab: any) => tab.label), ); - tabs = [ + tabs = { ...tabs, - { + [newTabId]: { id: newTabId, label: newTabLabel, widgetId: generateReactKey(), isVisible: true, }, - ]; + }; this.updateProperty(this.props.propertyName, tabs); }; diff --git a/app/client/src/constants/FieldExpectedValue.ts b/app/client/src/constants/FieldExpectedValue.ts index e75ddd092d..6ae293519e 100644 --- a/app/client/src/constants/FieldExpectedValue.ts +++ b/app/client/src/constants/FieldExpectedValue.ts @@ -13,6 +13,7 @@ const FIELD_VALUES: Record< CANVAS_WIDGET: {}, ICON_WIDGET: {}, SKELETON_WIDGET: {}, + TABS_MIGRATOR_WIDGET: {}, CONTAINER_WIDGET: { backgroundColor: "string", isVisible: "boolean", @@ -61,8 +62,6 @@ const FIELD_VALUES: Record< // onSelectionChange: "Function Call", }, TABS_WIDGET: { - tabs: - "Array<{ label: string, id: string(unique), widgetId: string(unique) }>", selectedTab: "string", isVisible: "boolean", }, diff --git a/app/client/src/constants/HelpConstants.ts b/app/client/src/constants/HelpConstants.ts index 19e72b7c50..ecd9087c09 100644 --- a/app/client/src/constants/HelpConstants.ts +++ b/app/client/src/constants/HelpConstants.ts @@ -83,6 +83,10 @@ export const HelpMap = { path: "", searchKey: "Tabs", }, + TABS_MIGRATOR_WIDGET: { + path: "", + searchKey: "", + }, MODAL_WIDGET: { path: "", searchKey: "", diff --git a/app/client/src/constants/ReduxActionConstants.tsx b/app/client/src/constants/ReduxActionConstants.tsx index 53c5f7dc8e..1da9d860e9 100644 --- a/app/client/src/constants/ReduxActionConstants.tsx +++ b/app/client/src/constants/ReduxActionConstants.tsx @@ -340,6 +340,7 @@ export const ReduxActionTypes: { [key: string]: string } = { UNDO_DELETE_WIDGET: "UNDO_DELETE_WIDGET", CUT_SELECTED_WIDGET: "CUT_SELECTED_WIDGET", WIDGET_ADD_CHILDREN: "WIDGET_ADD_CHILDREN", + WIDGET_UPDATE_PROPERTY: "WIDGET_UPDATE_PROPERTY", SET_EVALUATED_TREE: "SET_EVALUATED_TREE", SET_EVALUATION_INVERSE_DEPENDENCY_MAP: "SET_EVALUATION_INVERSE_DEPENDENCY_MAP", diff --git a/app/client/src/constants/WidgetConstants.tsx b/app/client/src/constants/WidgetConstants.tsx index 3f066457bd..8d477e78d7 100644 --- a/app/client/src/constants/WidgetConstants.tsx +++ b/app/client/src/constants/WidgetConstants.tsx @@ -26,6 +26,7 @@ export enum WidgetTypes { SKELETON_WIDGET = "SKELETON_WIDGET", LIST_WIDGET = "LIST_WIDGET", SWITCH_WIDGET = "SWITCH_WIDGET", + TABS_MIGRATOR_WIDGET = "TABS_MIGRATOR_WIDGET", } export type WidgetType = keyof typeof WidgetTypes; diff --git a/app/client/src/constants/WidgetValidation.ts b/app/client/src/constants/WidgetValidation.ts index 1f5d32dbbf..56453a5003 100644 --- a/app/client/src/constants/WidgetValidation.ts +++ b/app/client/src/constants/WidgetValidation.ts @@ -16,7 +16,6 @@ export enum VALIDATION_TYPES { DEFAULT_DATE = "DEFAULT_DATE", MIN_DATE = "MIN_DATE", MAX_DATE = "MAX_DATE", - TABS_DATA = "TABS_DATA", LIST_DATA = "LIST_DATA", CHART_SERIES_DATA = "CHART_SERIES_DATA", CUSTOM_FUSION_CHARTS_DATA = "CUSTOM_FUSION_CHARTS_DATA", @@ -30,6 +29,7 @@ export enum VALIDATION_TYPES { LAT_LONG = "LAT_LONG", TABLE_PAGE_NO = "TABLE_PAGE_NO", ROW_INDICES = "ROW_INDICES", + TABS_DATA = "TABS_DATA", } export type ValidationResponse = { @@ -43,6 +43,7 @@ export type Validator = ( value: any, props: WidgetProps, dataTree?: DataTree, + property?: string, ) => ValidationResponse; export const ISO_DATE_FORMAT = "YYYY-MM-DDTHH:mm:ss.sssZ"; diff --git a/app/client/src/mockResponses/WidgetConfigResponse.tsx b/app/client/src/mockResponses/WidgetConfigResponse.tsx index 61951129e5..6dc38a87f6 100644 --- a/app/client/src/mockResponses/WidgetConfigResponse.tsx +++ b/app/client/src/mockResponses/WidgetConfigResponse.tsx @@ -236,10 +236,22 @@ const WidgetConfigResponse: WidgetConfigReducerState = { columns: 8, shouldScrollContents: false, widgetName: "Tabs", - tabs: [ - { label: "Tab 1", id: "tab1", widgetId: "", isVisible: true }, - { label: "Tab 2", id: "tab2", widgetId: "", isVisible: true }, - ], + tabsObj: { + tab1: { + label: "Tab 1", + id: "tab1", + widgetId: "", + isVisible: true, + index: 0, + }, + tab2: { + label: "Tab 2", + id: "tab2", + widgetId: "", + isVisible: true, + index: 1, + }, + }, shouldShowTabs: true, defaultTab: "Tab 1", blueprint: { @@ -247,18 +259,18 @@ const WidgetConfigResponse: WidgetConfigReducerState = { { type: BlueprintOperationTypes.MODIFY_PROPS, fn: (widget: WidgetProps & { children?: WidgetProps[] }) => { - const tabs = [...widget.tabs]; - - const newTabs = tabs.map((tab: any) => { + const tabs = Object.values({ ...widget.tabsObj }); + const tabsObj = tabs.reduce((obj: any, tab: any) => { const newTab = { ...tab }; newTab.widgetId = generateReactKey(); - return newTab; - }); + obj[newTab.id] = newTab; + return obj; + }, {}); const updatePropertyMap = [ { widgetId: widget.widgetId, - propertyName: "tabs", - propertyValue: newTabs, + propertyName: "tabsObj", + propertyValue: tabsObj, }, ]; return updatePropertyMap; @@ -266,7 +278,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { }, ], }, - version: 1, + version: 2, }, MODAL_WIDGET: { rows: 6, @@ -511,6 +523,13 @@ const WidgetConfigResponse: WidgetConfigReducerState = { widgetName: "Skeleton", version: 1, }, + TABS_MIGRATOR_WIDGET: { + isLoading: true, + rows: 1, + columns: 1, + widgetName: "Skeleton", + version: 1, + }, [WidgetTypes.LIST_WIDGET]: { backgroundColor: "", itemBackgroundColor: "white", diff --git a/app/client/src/pages/Editor/Explorer/Entity/CurrentPageEntityProperties.tsx b/app/client/src/pages/Editor/Explorer/Entity/CurrentPageEntityProperties.tsx index 05a92bc622..75c12f4681 100644 --- a/app/client/src/pages/Editor/Explorer/Entity/CurrentPageEntityProperties.tsx +++ b/app/client/src/pages/Editor/Explorer/Entity/CurrentPageEntityProperties.tsx @@ -68,7 +68,10 @@ export const CurrentPageEntityProperties = memo( case ENTITY_TYPE.WIDGET: const type: Exclude< Partial, - "CANVAS_WIDGET" | "ICON_WIDGET" | "SKELETON_WIDGET" + | "CANVAS_WIDGET" + | "ICON_WIDGET" + | "SKELETON_WIDGET" + | "TABS_MIGRATOR_WIDGET" > = entity.type; config = entityDefinitions[type]; if (!config) { diff --git a/app/client/src/pages/Editor/Explorer/Entity/EntityProperties.tsx b/app/client/src/pages/Editor/Explorer/Entity/EntityProperties.tsx index ac21089eb0..d32e2ac34a 100644 --- a/app/client/src/pages/Editor/Explorer/Entity/EntityProperties.tsx +++ b/app/client/src/pages/Editor/Explorer/Entity/EntityProperties.tsx @@ -76,7 +76,10 @@ export const EntityProperties = memo( case ENTITY_TYPE.WIDGET: const type: Exclude< Partial, - "CANVAS_WIDGET" | "ICON_WIDGET" | "SKELETON_WIDGET" + | "CANVAS_WIDGET" + | "ICON_WIDGET" + | "SKELETON_WIDGET" + | "TABS_MIGRATOR_WIDGET" > = entity.type; config = entityDefinitions[type]; if (!config) { diff --git a/app/client/src/pages/Editor/Explorer/Entity/Name.tsx b/app/client/src/pages/Editor/Explorer/Entity/Name.tsx index fbaba5cc46..eb000a7d2a 100644 --- a/app/client/src/pages/Editor/Explorer/Entity/Name.tsx +++ b/app/client/src/pages/Editor/Explorer/Entity/Name.tsx @@ -83,7 +83,7 @@ export const EntityName = forwardRef( ) { const parent = state.entities.canvasWidgets[widget.parentId]; if (parent.type === WidgetTypes.TABS_WIDGET) { - return parent.tabs; + return Object.values(parent.tabsObj); } } } diff --git a/app/client/src/pages/Editor/Explorer/Widgets/WidgetContextMenu.tsx b/app/client/src/pages/Editor/Explorer/Widgets/WidgetContextMenu.tsx index e3c61d4b0b..c28bf0bfae 100644 --- a/app/client/src/pages/Editor/Explorer/Widgets/WidgetContextMenu.tsx +++ b/app/client/src/pages/Editor/Explorer/Widgets/WidgetContextMenu.tsx @@ -33,16 +33,15 @@ export const WidgetContextMenu = (props: { // If the widget is a tab we are updating the `tabs` of the property of the widget // This is similar to deleting a tab from the property pane if (widget.tabName && parentWidget.type === WidgetTypes.TABS_WIDGET) { - const filteredTabs = parentWidget.tabs.filter( - (tab: any) => tab.widgetId !== widgetId, - ); - + const tabsObj = { ...parentWidget.tabsObj }; + delete tabsObj[widget.tabId]; + const filteredTabs = Object.values(tabsObj); if (widget.parentId && !!filteredTabs.length) { dispatch( updateWidgetPropertyRequest( widget.parentId, - "tabs", - filteredTabs, + "tabsObj", + tabsObj, RenderModes.CANVAS, ), ); diff --git a/app/client/src/pages/Editor/PropertyPane/PanelPropertiesEditor.tsx b/app/client/src/pages/Editor/PropertyPane/PanelPropertiesEditor.tsx index a2503c7c08..91585346e6 100644 --- a/app/client/src/pages/Editor/PropertyPane/PanelPropertiesEditor.tsx +++ b/app/client/src/pages/Editor/PropertyPane/PanelPropertiesEditor.tsx @@ -127,7 +127,7 @@ export const PanelPropertiesEditor = ( }, [widgetProperties, panelParentPropertyPath, panelProps, panelConfig]); const panelConfigs = useMemo(() => { - if (currentIndex) { + if (currentIndex !== undefined) { let path: string | undefined = undefined; if (isString(currentIndex)) { path = `${panelParentPropertyPath}.${currentIndex}`; @@ -147,7 +147,7 @@ export const PanelPropertiesEditor = ( ); useEffect(() => { - if (panelProps.widgetId !== widgetProperties.widgetId) { + if (panelProps.propPaneId !== widgetProperties.widgetId) { props.closePanel(); } }, [widgetProperties.widgetId]); diff --git a/app/client/src/pages/Editor/PropertyPane/PropertyControl.tsx b/app/client/src/pages/Editor/PropertyPane/PropertyControl.tsx index 94ed5fd93e..9cb33fdf35 100644 --- a/app/client/src/pages/Editor/PropertyPane/PropertyControl.tsx +++ b/app/client/src/pages/Editor/PropertyPane/PropertyControl.tsx @@ -249,12 +249,13 @@ const PropertyControl = memo((props: Props) => { let validationMessage = ""; if (widgetProperties) { isValid = widgetProperties.invalidProps - ? !(propertyName in widgetProperties.invalidProps) + ? !_.has(widgetProperties.invalidProps, propertyName) : true; - validationMessage = widgetProperties.validationMessages - ? propertyName in widgetProperties.validationMessages - ? widgetProperties.validationMessages[propertyName] - : "" + const validationMsgPresent = + widgetProperties.validationMessages && + _.has(widgetProperties.validationMessages, propertyName); + validationMessage = validationMsgPresent + ? _.get(widgetProperties.validationMessages, propertyName) : ""; } return { isValid, validationMessage }; diff --git a/app/client/src/reducers/entityReducers/widgetConfigReducer.tsx b/app/client/src/reducers/entityReducers/widgetConfigReducer.tsx index f112bae987..7b8c5b021a 100644 --- a/app/client/src/reducers/entityReducers/widgetConfigReducer.tsx +++ b/app/client/src/reducers/entityReducers/widgetConfigReducer.tsx @@ -18,7 +18,7 @@ import { FilePickerWidgetProps } from "../../widgets/FilepickerWidget"; import { TabsWidgetProps, TabContainerWidgetProps, -} from "../../widgets/TabsWidget"; +} from "../../widgets/Tabs/TabsWidget"; import { ChartWidgetProps } from "widgets/ChartWidget"; import { FormWidgetProps } from "widgets/FormWidget"; import { FormButtonWidgetProps } from "widgets/FormButtonWidget"; @@ -70,6 +70,8 @@ export interface WidgetConfigReducerState { FILE_PICKER_WIDGET: Partial & WidgetConfigProps; TABS_WIDGET: Partial> & WidgetConfigProps; + TABS_MIGRATOR_WIDGET: Partial> & + WidgetConfigProps; MODAL_WIDGET: Partial & WidgetConfigProps; CHART_WIDGET: Partial & WidgetConfigProps; FORM_WIDGET: Partial & WidgetConfigProps; diff --git a/app/client/src/sagas/PageSagas.tsx b/app/client/src/sagas/PageSagas.tsx index b99f3d048f..d8d0c5c06b 100644 --- a/app/client/src/sagas/PageSagas.tsx +++ b/app/client/src/sagas/PageSagas.tsx @@ -152,7 +152,7 @@ export function* fetchPageListSaga( } } -const getCanvasWidgetsPayload = ( +export const getCanvasWidgetsPayload = ( pageResponse: FetchPageResponse, ): UpdateCanvasPayload => { const normalizedResponse = CanvasWidgetsNormalizer.normalize( @@ -596,13 +596,14 @@ export function* updateWidgetNameSaga( // TODO(abhinav): Why do we need to jump through these hoops just to // change the tab name? Figure out a better design to make this moot. - const tabs: - | Array<{ - id: string; - widgetId: string; - label: string; - }> - | undefined = yield select((state: AppState) => { + const tabsObj: Record< + string, + { + id: string; + widgetId: string; + label: string; + } + > = yield select((state: AppState) => { // Check if this widget exists in the canvas widgets if (state.entities.canvasWidgets.hasOwnProperty(action.payload.id)) { // If it does assign it to a variable @@ -617,7 +618,7 @@ export function* updateWidgetNameSaga( // Check if this parent is a TABS_WIDGET if (parent.type === WidgetTypes.TABS_WIDGET) { // If it is return the tabs property - return parent.tabs; + return parent.tabsObj; } } } @@ -626,7 +627,8 @@ export function* updateWidgetNameSaga( }); // If we're trying to update the name of a tab in the TABS_WIDGET - if (tabs !== undefined) { + if (tabsObj !== undefined) { + const tabs: any = Object.values(tabsObj); // Get all canvas widgets const stateWidgets = yield select(getWidgets); // Shallow copy canvas widgets as they're immutable @@ -641,14 +643,19 @@ export function* updateWidgetNameSaga( // Shallow copy the parent widget so that we can update the properties const parent = { ...widgets[parentId] }; // Update the tabs property of the parent tabs widget - parent.tabs = tabs.map( - (tab: { widgetId: string; label: string; id: string }) => { - if (tab.widgetId === action.payload.id) { - return { ...tab, label: action.payload.newName }; - } - return tab; - }, + const tabToChange = tabs.find( + (each: any) => each.widgetId === action.payload.id, ); + const updatedTab = { + ...tabToChange, + label: action.payload.newName, + }; + parent.tabsObj = { + ...parent.tabsObj, + [updatedTab.id]: { + ...updatedTab, + }, + }; // replace the parent widget in the canvas widgets widgets[parentId] = parent; // Update and save the new widgets diff --git a/app/client/src/sagas/WidgetOperationSagas.tsx b/app/client/src/sagas/WidgetOperationSagas.tsx index 567674744c..bc2343928f 100644 --- a/app/client/src/sagas/WidgetOperationSagas.tsx +++ b/app/client/src/sagas/WidgetOperationSagas.tsx @@ -593,20 +593,21 @@ export function* undoDeleteSaga(action: ReduxAction<{ widgetId: string }>) { widget.type === WidgetTypes.CANVAS_WIDGET && widget.parentId ) { - const parent = { ...widgets[widget.parentId] }; - if (parent.tabs) { - parent.tabs = parent.tabs.slice(); + const parent = cloneDeep(widgets[widget.parentId]); + if (parent.tabsObj) { try { - parent.tabs.push({ + const tabs = Object.values(parent.tabsObj); + parent.tabsObj[widget.tabId] = { id: widget.tabId, widgetId: widget.widgetId, label: widget.tabName || widget.widgetName, - }); + isVisible: true, + }; widgets = { ...widgets, [widget.parentId]: { ...widgets[widget.parentId], - tabs: parent.tabs, + tabsObj: parent.tabsObj, }, }; } catch (error) { @@ -866,7 +867,7 @@ function* setWidgetDynamicPropertySaga( ) { const { isDynamic, propertyPath, widgetId } = action.payload; const stateWidget: WidgetProps = yield select(getWidget, widgetId); - let widget = { ...stateWidget }; + let widget = cloneDeep({ ...stateWidget }); const propertyValue = _.get(widget, propertyPath); let dynamicPropertyPathList = getWidgetDynamicPropertyPathList(widget); if (isDynamic) { @@ -1041,6 +1042,9 @@ function* removeWidgetProperties(widget: WidgetProps, paths: string[]) { let dynamicBindingPathList: DynamicPath[] = getEntityDynamicBindingPathList( widget, ); + let dynamicPropertyPathList: DynamicPath[] = getWidgetDynamicPropertyPathList( + widget, + ); paths.forEach((propertyPath) => { dynamicTriggerPathList = dynamicTriggerPathList.filter((dynamicPath) => { @@ -1050,10 +1054,18 @@ function* removeWidgetProperties(widget: WidgetProps, paths: string[]) { dynamicBindingPathList = dynamicBindingPathList.filter((dynamicPath) => { return !isChildPropertyPath(propertyPath, dynamicPath.key); }); + + dynamicPropertyPathList = dynamicPropertyPathList.filter( + (dynamicPath) => { + return !isChildPropertyPath(propertyPath, dynamicPath.key); + }, + ); }); widget.dynamicBindingPathList = dynamicBindingPathList; widget.dynamicTriggerPathList = dynamicTriggerPathList; + widget.dynamicPropertyPathList = dynamicPropertyPathList; + paths.forEach((propertyPath) => { widget = unsetPropertyPath(widget, propertyPath) as WidgetProps; }); @@ -1389,14 +1401,15 @@ function* pasteWidgetSaga() { } // Update the tabs for the tabs widget. - if (widget.tabs && widget.type === WidgetTypes.TABS_WIDGET) { + if (widget.tabsObj && widget.type === WidgetTypes.TABS_WIDGET) { try { - const tabs = widget.tabs; + const tabs = Object.values(widget.tabsObj); if (Array.isArray(tabs)) { - widget.tabs = tabs.map((tab) => { + widget.tabsObj = tabs.reduce((obj: any, tab) => { tab.widgetId = widgetIdMap[tab.widgetId]; - return tab; - }); + obj[tab.id] = tab; + return obj; + }, {}); } } catch (error) { log.debug("Error updating tabs", error); @@ -1692,6 +1705,10 @@ export default function* widgetOperationSagas() { ReduxActionTypes.UPDATE_WIDGET_PROPERTY_REQUEST, updateWidgetPropertySaga, ), + takeEvery( + ReduxActionTypes.WIDGET_UPDATE_PROPERTY, + updateWidgetPropertySaga, + ), takeEvery( ReduxActionTypes.SET_WIDGET_DYNAMIC_PROPERTY, setWidgetDynamicPropertySaga, diff --git a/app/client/src/selectors/propertyPaneSelectors.tsx b/app/client/src/selectors/propertyPaneSelectors.tsx index 055e7dd560..0c8d3b453e 100644 --- a/app/client/src/selectors/propertyPaneSelectors.tsx +++ b/app/client/src/selectors/propertyPaneSelectors.tsx @@ -58,7 +58,12 @@ export const getWidgetPropsForPropertyPane = createSelector( }, ); +const isResizingorDragging = (state: AppState) => + state.ui.widgetDragResize.isResizing || state.ui.widgetDragResize.isDragging; + export const getIsPropertyPaneVisible = createSelector( getPropertyPaneState, - (pane: PropertyPaneReduxState) => !!(pane.isVisible && pane.widgetId), + isResizingorDragging, + (pane: PropertyPaneReduxState, isResizingorDragging: boolean) => + !!(!isResizingorDragging && pane.isVisible && pane.widgetId), ); diff --git a/app/client/src/utils/WidgetPropsUtils.tsx b/app/client/src/utils/WidgetPropsUtils.tsx index 2cc76087ed..ef6498b393 100644 --- a/app/client/src/utils/WidgetPropsUtils.tsx +++ b/app/client/src/utils/WidgetPropsUtils.tsx @@ -31,6 +31,7 @@ import { migrateIncorrectDynamicBindingPathLists } from "utils/migrations/Incorr import * as Sentry from "@sentry/react"; import { migrateTextStyleFromTextWidget } from "./migrations/TextWidgetReplaceTextStyle"; import { nextAvailableRowInContainer } from "entities/Widget/utils"; +import { DATA_BIND_REGEX_GLOBAL } from "constants/BindingsConstants"; export type WidgetOperationParams = { operation: WidgetOperation; @@ -333,6 +334,111 @@ const rteDefaultValueMigration = ( return currentDSL; }; +function migrateTabsDataUsingMigrator( + currentDSL: ContainerWidgetProps, +) { + if (currentDSL.type === WidgetTypes.TABS_WIDGET && currentDSL.version === 1) { + try { + currentDSL.type = WidgetTypes.TABS_MIGRATOR_WIDGET; + currentDSL.version = 1; + } catch (error) { + Sentry.captureException({ + message: "Tabs Migration Failed", + oldData: currentDSL.tabs, + }); + currentDSL.tabsObj = {}; + delete currentDSL.tabs; + } + } + if (currentDSL.children && currentDSL.children.length) { + currentDSL.children = currentDSL.children.map(migrateTabsDataUsingMigrator); + } + return currentDSL; +} + +export function migrateTabsData(currentDSL: ContainerWidgetProps) { + if ( + [WidgetTypes.TABS_WIDGET, WidgetTypes.TABS_MIGRATOR_WIDGET].includes( + currentDSL.type as any, + ) && + currentDSL.version === 1 + ) { + try { + currentDSL.type = WidgetTypes.TABS_WIDGET; + const isTabsDataBinded = isString(currentDSL.tabs); + currentDSL.dynamicPropertyPathList = + currentDSL.dynamicPropertyPathList || []; + currentDSL.dynamicBindingPathList = + currentDSL.dynamicBindingPathList || []; + + if (isTabsDataBinded) { + const tabsString = currentDSL.tabs.replace( + DATA_BIND_REGEX_GLOBAL, + (word: any) => `"${word}"`, + ); + try { + currentDSL.tabs = JSON.parse(tabsString); + } catch (error) { + return migrateTabsDataUsingMigrator(currentDSL); + } + const dynamicPropsList = currentDSL.tabs + .filter((each: any) => DATA_BIND_REGEX_GLOBAL.test(each.isVisible)) + .map((each: any) => { + return { key: `tabsObj.${each.id}.isVisible` }; + }); + const dynamicBindablePropsList = currentDSL.tabs.map((each: any) => { + return { key: `tabsObj.${each.id}.isVisible` }; + }); + currentDSL.dynamicPropertyPathList = [ + ...currentDSL.dynamicPropertyPathList, + ...dynamicPropsList, + ]; + currentDSL.dynamicBindingPathList = [ + ...currentDSL.dynamicBindingPathList, + ...dynamicBindablePropsList, + ]; + } + currentDSL.dynamicPropertyPathList = currentDSL.dynamicPropertyPathList.filter( + (each) => { + return each.key !== "tabs"; + }, + ); + currentDSL.dynamicBindingPathList = currentDSL.dynamicBindingPathList.filter( + (each) => { + return each.key !== "tabs"; + }, + ); + currentDSL.tabsObj = currentDSL.tabs.reduce( + (obj: any, tab: any, index: number) => { + obj = { + ...obj, + [tab.id]: { + ...tab, + isVisible: tab.isVisible === undefined ? true : tab.isVisible, + index, + }, + }; + return obj; + }, + {}, + ); + currentDSL.version = 2; + delete currentDSL.tabs; + } catch (error) { + Sentry.captureException({ + message: "Tabs Migration Failed", + oldData: currentDSL.tabs, + }); + currentDSL.tabsObj = {}; + delete currentDSL.tabs; + } + } + if (currentDSL.children && currentDSL.children.length) { + currentDSL.children = currentDSL.children.map(migrateTabsData); + } + return currentDSL; +} + // A rudimentary transform function which updates the DSL based on its version. function migrateOldChartData(currentDSL: ContainerWidgetProps) { if (currentDSL.type === WidgetTypes.CHART_WIDGET) { @@ -543,6 +649,11 @@ const transformDSL = (currentDSL: ContainerWidgetProps) => { currentDSL.version = 17; } + if (currentDSL.version === 17) { + currentDSL = migrateTabsData(currentDSL); + currentDSL.version = 18; + } + return currentDSL; }; diff --git a/app/client/src/utils/WidgetRegistry.tsx b/app/client/src/utils/WidgetRegistry.tsx index 797f986691..7f735ff666 100644 --- a/app/client/src/utils/WidgetRegistry.tsx +++ b/app/client/src/utils/WidgetRegistry.tsx @@ -44,7 +44,7 @@ import TabsWidget, { TabsWidgetProps, TabContainerWidgetProps, ProfiledTabsWidget, -} from "widgets/TabsWidget"; +} from "widgets/Tabs/TabsWidget"; import { ModalWidgetProps, ProfiledModalWidget, @@ -100,6 +100,9 @@ import SwitchWidget, { ProfiledSwitchWidget, SwitchWidgetProps, } from "widgets/SwitchWidget"; +import TabsMigratorWidget, { + ProfiledTabsMigratorWidget, +} from "widgets/Tabs/TabsMigrator"; export default class WidgetBuilderRegistry { static registerWidgetBuilders() { WidgetFactory.registerWidgetBuilder( @@ -296,6 +299,20 @@ export default class WidgetBuilderRegistry { TabsWidget.getMetaPropertiesMap(), TabsWidget.getPropertyPaneConfig(), ); + WidgetFactory.registerWidgetBuilder( + "TABS_MIGRATOR_WIDGET", + { + buildWidget( + widgetProps: TabsWidgetProps, + ): JSX.Element { + return ; + }, + }, + TabsMigratorWidget.getDerivedPropertiesMap(), + TabsMigratorWidget.getDefaultPropertiesMap(), + TabsMigratorWidget.getMetaPropertiesMap(), + TabsMigratorWidget.getPropertyPaneConfig(), + ); WidgetFactory.registerWidgetBuilder( WidgetTypes.MODAL_WIDGET, { diff --git a/app/client/src/utils/autocomplete/EntityDefinitions.ts b/app/client/src/utils/autocomplete/EntityDefinitions.ts index 33bbfacb48..9f7e72f86e 100644 --- a/app/client/src/utils/autocomplete/EntityDefinitions.ts +++ b/app/client/src/utils/autocomplete/EntityDefinitions.ts @@ -170,7 +170,6 @@ export const entityDefinitions = { }, TABS_WIDGET: { isVisible: isVisible, - tabs: "[tabs]", selectedTab: "string", }, MODAL_WIDGET: { diff --git a/app/client/src/widgets/Tabs/TabsMigrator.tsx b/app/client/src/widgets/Tabs/TabsMigrator.tsx new file mode 100644 index 0000000000..14f4f5888b --- /dev/null +++ b/app/client/src/widgets/Tabs/TabsMigrator.tsx @@ -0,0 +1,110 @@ +import BaseWidget, { WidgetState } from "widgets/BaseWidget"; +import { TabContainerWidgetProps, TabsWidgetProps } from "./TabsWidget"; +import React from "react"; +import { WidgetType, WidgetTypes } from "constants/WidgetConstants"; +import withMeta from "widgets/MetaHOC"; +import * as Sentry from "@sentry/react"; +import { migrateTabsData } from "utils/WidgetPropsUtils"; +import { cloneDeep } from "lodash"; +import { VALIDATION_TYPES } from "constants/WidgetValidation"; + +class TabsMigratorWidget extends BaseWidget< + TabsWidgetProps, + WidgetState +> { + getPageView() { + return <>; + } + static getPropertyPaneConfig() { + return [ + { + sectionName: "General", + children: [ + { + helpText: "Takes an array of tab names to render tabs", + propertyName: "tabs", + isJSConvertible: true, + label: "Tabs", + controlType: "TABS_INPUT", + isBindProperty: true, + isTriggerProperty: false, + validation: VALIDATION_TYPES.TABS_DATA, + }, + { + propertyName: "shouldShowTabs", + helpText: + "Hides the tabs so that different widgets can be displayed based on the default tab", + label: "Show Tabs", + controlType: "SWITCH", + isBindProperty: false, + isTriggerProperty: false, + }, + { + propertyName: "defaultTab", + helpText: "Selects a tab name specified by default", + placeholderText: "Enter tab name", + label: "Default Tab", + controlType: "INPUT_TEXT", + isBindProperty: true, + isTriggerProperty: false, + validation: VALIDATION_TYPES.SELECTED_TAB, + }, + { + propertyName: "shouldScrollContents", + label: "Scroll Contents", + 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, + validation: VALIDATION_TYPES.BOOLEAN, + }, + ], + }, + { + sectionName: "Actions", + children: [ + { + helpText: "Triggers an action when the button is clicked", + propertyName: "onTabSelected", + label: "onTabSelected", + controlType: "ACTION_SELECTOR", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: true, + }, + ], + }, + ]; + } + componentDidMount() { + if (this.props.evaluatedValues) { + const tabsDsl = cloneDeep(this.props); + const migratedTabsDsl = migrateTabsData(tabsDsl); + this.batchUpdateWidgetProperty({ + modify: { + tabsObj: migratedTabsDsl.tabsObj, + type: WidgetTypes.TABS_WIDGET, + version: 2, + dynamicPropertyPathList: migratedTabsDsl.dynamicPropertyPathList, + dynamicBindingPathList: migratedTabsDsl.dynamicBindingPathList, + }, + remove: ["tabs"], + }); + } + } + getWidgetType(): WidgetType { + return "TABS_MIGRATOR_WIDGET"; + } +} +export default TabsMigratorWidget; +export const ProfiledTabsMigratorWidget = Sentry.withProfiler( + withMeta(TabsMigratorWidget), +); diff --git a/app/client/src/widgets/Tabs/TabsWidget.test.tsx b/app/client/src/widgets/Tabs/TabsWidget.test.tsx new file mode 100644 index 0000000000..b9bae0720a --- /dev/null +++ b/app/client/src/widgets/Tabs/TabsWidget.test.tsx @@ -0,0 +1,98 @@ +import { + buildChildren, + widgetCanvasFactory, +} from "test/factories/WidgetFactoryUtils"; +import { render, fireEvent } from "test/testUtils"; + +import Canvas from "pages/Editor/Canvas"; +import React from "react"; +import { useDispatch } from "react-redux"; +import { editorInitializer } from "utils/EditorUtils"; +import { initCanvasLayout } from "actions/pageActions"; +import { getCanvasWidgetsPayload } from "sagas/PageSagas"; +import { noop } from "utils/AppsmithUtils"; + +Element.prototype.scrollTo = noop; +const SetCanvas = ({ dsl, children }: any) => { + const dispatch = useDispatch(); + const mockResp: any = { + data: { + id: "asa", + name: "App", + applicationId: "asa", + layouts: [ + { + id: "w323", + dsl, + layoutOnLoadActions: [], + layoutActions: [], + }, + ], + }, + }; + const canvasWidgetsPayload = getCanvasWidgetsPayload(mockResp); + dispatch(initCanvasLayout(canvasWidgetsPayload)); + return <>{children}; +}; +describe("Tabs widget functional cases", () => { + it("Should render 2 tabs by default", () => { + editorInitializer(); + const children: any = buildChildren([{ type: "TABS_WIDGET" }]); + const dsl: any = widgetCanvasFactory.build({ + children, + }); + const component = render( + + + , + ); + const tab1 = component.queryByText("Tab 1"); + const tab2 = component.queryByText("Tab 2"); + expect(tab1).toBeDefined(); + expect(tab2).toBeDefined(); + }); + + it("Should render components inside tabs by default", () => { + editorInitializer(); + const tab1Children = buildChildren([ + { type: "SWITCH_WIDGET", label: "Tab1 Switch" }, + { type: "CHECKBOX_WIDGET", label: "Tab1 Checkbox" }, + ]); + const tab2Children = buildChildren([ + { type: "INPUT_WIDGET", text: "Tab2 Text" }, + { type: "BUTTON_WIDGET", label: "Tab2 Button" }, + ]); + const children: any = buildChildren([{ type: "TABS_WIDGET" }]); + children[0].children[0].children = tab1Children; + children[0].children[1].children = tab2Children; + const dsl: any = widgetCanvasFactory.build({ + children, + }); + const component = render( + + + , + ); + const tab1 = component.queryByText("Tab 1"); + const tab2: any = component.queryByText("Tab 2"); + expect(tab1).toBeDefined(); + expect(tab2).toBeDefined(); + let tab1Switch = component.queryByText("Tab1 Switch"); + let tab1Checkbox = component.queryByText("Tab1 Checkbox"); + let tab2Input = component.queryByText("Tab2 Text"); + let tab2Button = component.queryByText("Tab2 Button"); + expect(tab1Switch).toBeDefined(); + expect(tab1Checkbox).toBeDefined(); + expect(tab2Input).toBeNull(); + expect(tab2Button).toBeNull(); + fireEvent.click(tab2); + tab1Switch = component.queryByText("Tab1 Switch"); + tab1Checkbox = component.queryByText("Tab1 Checkbox"); + tab2Input = component.queryByText("Tab2 Text"); + tab2Button = component.queryByText("Tab2 Button"); + expect(tab1Switch).toBeNull(); + expect(tab1Checkbox).toBeNull(); + expect(tab2Input).toBeDefined(); + expect(tab2Button).toBeDefined(); + }); +}); diff --git a/app/client/src/widgets/TabsWidget.tsx b/app/client/src/widgets/Tabs/TabsWidget.tsx similarity index 73% rename from app/client/src/widgets/TabsWidget.tsx rename to app/client/src/widgets/Tabs/TabsWidget.tsx index 7c14fcbaa5..bd71ad9e62 100644 --- a/app/client/src/widgets/TabsWidget.tsx +++ b/app/client/src/widgets/Tabs/TabsWidget.tsx @@ -1,7 +1,7 @@ import React from "react"; import TabsComponent from "components/designSystems/appsmith/TabsComponent"; import { WidgetType, WidgetTypes } from "constants/WidgetConstants"; -import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget"; +import BaseWidget, { WidgetProps, WidgetState } from "../BaseWidget"; import WidgetFactory from "utils/WidgetFactory"; import { VALIDATION_TYPES } from "constants/WidgetValidation"; import _ from "lodash"; @@ -9,7 +9,7 @@ import { EventType } from "constants/AppsmithActionConstants/ActionConstants"; import { WidgetOperations } from "widgets/BaseWidget"; import * as Sentry from "@sentry/react"; import { generateReactKey } from "utils/generators"; -import withMeta, { WithMeta } from "./MetaHOC"; +import withMeta, { WithMeta } from "../MetaHOC"; class TabsWidget extends BaseWidget< TabsWidgetProps, @@ -22,13 +22,47 @@ class TabsWidget extends BaseWidget< children: [ { helpText: "Takes an array of tab names to render tabs", - propertyName: "tabs", - isJSConvertible: true, + propertyName: "tabsObj", + isJSConvertible: false, label: "Tabs", controlType: "TABS_INPUT", - isBindProperty: true, + isBindProperty: false, isTriggerProperty: false, - validation: VALIDATION_TYPES.TABS_DATA, + panelConfig: { + editableTitle: true, + titlePropertyName: "label", + panelIdPropertyName: "id", + updateHook: ( + props: any, + propertyPath: string, + propertyValue: string, + ) => { + return [ + { + propertyPath, + propertyValue, + }, + ]; + }, + children: [ + { + sectionName: "Tab Control", + children: [ + { + propertyName: "isVisible", + label: "Visible", + helpText: "Controls the visibility of the widget", + controlType: "SWITCH", + useValidationMessage: true, + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: VALIDATION_TYPES.BOOLEAN, + }, + ], + }, + ], + }, }, { propertyName: "defaultTab", @@ -97,7 +131,9 @@ class TabsWidget extends BaseWidget< static getDerivedPropertiesMap() { return { - selectedTab: `{{_.find(this.tabs, { widgetId: this.selectedTabWidgetId }).label}}`, + selectedTab: `{{_.find(Object.values(this.tabsObj), { + widgetId: this.selectedTabWidgetId, + }).label}}`, }; } @@ -152,8 +188,11 @@ class TabsWidget extends BaseWidget< } addTabContainer = (widgetIds: string[]) => { + const tabs = Object.values(this.props.tabsObj || {}); widgetIds.forEach((newWidgetId: string) => { - const tab = this.props.tabs.find((tab) => tab.widgetId === newWidgetId); + const tab = _.find(tabs, { + widgetId: newWidgetId, + }); if (tab) { const columns = (this.props.rightColumn - this.props.leftColumn) * @@ -186,6 +225,18 @@ class TabsWidget extends BaseWidget< }); }; + updateTabContainerNames = () => { + this.props.children.forEach((each) => { + const tab = this.props.tabsObj[each.tabId]; + if (tab && each.tabName !== tab.label) { + this.updateWidget(WidgetOperations.UPDATE_PROPERTY, each.widgetId, { + propertyPath: "tabName", + propertyValue: tab.label, + }); + } + }); + }; + removeTabContainer = (widgetIds: string[]) => { widgetIds.forEach((widgetIdToRemove: string) => { this.updateWidget(WidgetOperations.DELETE, widgetIdToRemove, { @@ -196,38 +247,47 @@ class TabsWidget extends BaseWidget< componentDidUpdate(prevProps: TabsWidgetProps) { if ( - Array.isArray(this.props.tabs) && - JSON.stringify(this.props.tabs) !== JSON.stringify(prevProps.tabs) + JSON.stringify(this.props.tabsObj) !== JSON.stringify(prevProps.tabsObj) ) { - const tabWidgetIds = this.props.tabs.map((tab) => tab.widgetId); + const tabWidgetIds = Object.values(this.props.tabsObj).map( + (tab) => tab.widgetId, + ); const childWidgetIds = this.props.children .filter(Boolean) .map((child) => child.widgetId); // If the tabs and children are different, // add and/or remove tab container widgets - if (!this.props.invalidProps?.tabs) { - if (_.xor(childWidgetIds, tabWidgetIds).length > 0) { - const widgetIdsToRemove: string[] = _.without( - childWidgetIds, - ...tabWidgetIds, - ); - const widgetIdsToCreate: string[] = _.without( - tabWidgetIds, - ...childWidgetIds, - ); + if (_.xor(childWidgetIds, tabWidgetIds).length > 0) { + const widgetIdsToRemove: string[] = _.without( + childWidgetIds, + ...tabWidgetIds, + ); + const widgetIdsToCreate: string[] = _.without( + tabWidgetIds, + ...childWidgetIds, + ); + if (widgetIdsToCreate && widgetIdsToCreate.length) { this.addTabContainer(widgetIdsToCreate); + } + if (widgetIdsToRemove && widgetIdsToRemove.length) { this.removeTabContainer(widgetIdsToRemove); } + } + this.updateTabContainerNames(); - // If all tabs were removed. - if (tabWidgetIds.length === 0) { - const newTabContainerWidgetId = generateReactKey(); - const tabs = [ - { id: "tab1", widgetId: newTabContainerWidgetId, label: "Tab 1" }, - ]; - this.updateWidgetProperty("tabs", tabs); - } + // If all tabs were removed. + if (tabWidgetIds.length === 0) { + const newTabContainerWidgetId = generateReactKey(); + const tabs = { + tab1: { + id: "tab1", + widgetId: newTabContainerWidgetId, + label: "Tab 1", + index: 0, + }, + }; + this.updateWidgetProperty("tabsObj", tabs); } } const visibleTabs = this.getVisibleTabs(); @@ -276,7 +336,8 @@ class TabsWidget extends BaseWidget< } generateTabContainers = () => { - const { tabs, widgetId } = this.props; + const { tabsObj, widgetId } = this.props; + const tabs = Object.values(tabsObj || {}); const childWidgetIds = this.props.children ?.filter(Boolean) .map((child) => child.widgetId); @@ -312,10 +373,13 @@ class TabsWidget extends BaseWidget< }; getVisibleTabs = () => { - if (Array.isArray(this.props.tabs)) { - return this.props.tabs.filter( - (tab) => tab.isVisible === undefined || tab.isVisible === true, - ); + const tabs = Object.values(this.props.tabsObj || {}); + if (tabs.length) { + return tabs + .filter( + (tab) => tab.isVisible === undefined || !!tab.isVisible === true, + ) + .sort((tab1, tab2) => tab1.index - tab2.index); } return []; }; @@ -323,7 +387,7 @@ class TabsWidget extends BaseWidget< componentDidMount() { const visibleTabs = this.getVisibleTabs(); // If we have a defaultTab - if (this.props.defaultTab && this.props.tabs?.length) { + if (this.props.defaultTab && Object.keys(this.props.tabsObj || {}).length) { // Find the default Tab object const selectedTab = _.find(visibleTabs, { label: this.props.defaultTab, @@ -345,7 +409,10 @@ class TabsWidget extends BaseWidget< selectedTabWidgetId, ); } - } else if (!this.props.selectedTabWidgetId && this.props.tabs?.length) { + } else if ( + !this.props.selectedTabWidgetId && + Object.keys(this.props.tabsObj || {}).length + ) { // If no tab is selected // Select the first tab in the tabs list. this.props.updateWidgetMetaProperty( @@ -372,6 +439,16 @@ export interface TabsWidgetProps widgetId: string; isVisible?: boolean; }>; + tabsObj: Record< + string, + { + id: string; + label: string; + widgetId: string; + isVisible?: boolean; + index: number; + } + >; shouldShowTabs: boolean; children: T[]; snapColumns?: number; diff --git a/app/client/src/workers/validations.ts b/app/client/src/workers/validations.ts index 12bea9d60c..52fafa33b6 100644 --- a/app/client/src/workers/validations.ts +++ b/app/client/src/workers/validations.ts @@ -741,18 +741,15 @@ export const VALIDATORS: Record = { value: any, props: WidgetProps, ): ValidationResponse => { - const tabs = - props.tabs && isString(props.tabs) - ? JSON.parse(props.tabs) - : props.tabs && Array.isArray(props.tabs) - ? props.tabs - : []; + const tabs: any = props.tabsObj + ? Object.values(props.tabsObj) + : props.tabs || []; const tabNames = tabs.map((i: { label: string; id: string }) => i.label); const isValidTabName = tabNames.includes(value); return { isValid: isValidTabName, - parsed: value, - message: isValidTabName ? "" : `Invalid tab name.`, + parsed: isValidTabName ? value : "", + message: isValidTabName ? "" : `Tab name provided does not exist.`, }; }, [VALIDATION_TYPES.DEFAULT_OPTION_VALUE]: ( diff --git a/app/client/test/__mocks__/derivedMock.js b/app/client/test/__mocks__/derivedMock.js new file mode 100644 index 0000000000..942acc9cfc --- /dev/null +++ b/app/client/test/__mocks__/derivedMock.js @@ -0,0 +1,3 @@ +import widgetPropertyFns from "!!raw-loader!./derived.js"; + +export default widgetPropertyFns; diff --git a/app/client/test/factories/WidgetFactoryUtils.ts b/app/client/test/factories/WidgetFactoryUtils.ts new file mode 100644 index 0000000000..55f06e9d59 --- /dev/null +++ b/app/client/test/factories/WidgetFactoryUtils.ts @@ -0,0 +1,28 @@ +import { makeFactory } from "factory.ts"; +import { WidgetProps } from "widgets/BaseWidget"; +import { ContainerWidgetProps } from "widgets/ContainerWidget"; +import defaultTemplate from "../../src/templates/default"; +import { WidgetTypeFactories } from "./Widgets/WidgetTypeFactories"; +const defaultMainContainer: ContainerWidgetProps = { + ...(defaultTemplate as any), + renderMode: "PAGE", + version: 1, + isLoading: false, +}; + +export const mainContainerFactory = makeFactory({ ...defaultMainContainer }); +export const widgetCanvasFactory = makeFactory(mainContainerFactory.build()); +const buildChild = (child: Partial): WidgetProps => { + return WidgetTypeFactories[child.type || "CANVAS_WIDGET"].build({ + ...child, + }); +}; +export const buildChildren = (children: Partial[]) => { + try { + return children.map((child) => { + return buildChild(child); + }); + } catch (error) { + console.error("Check if child widget data provided"); + } +}; diff --git a/app/client/test/factories/Widgets/ButtonFactory.ts b/app/client/test/factories/Widgets/ButtonFactory.ts new file mode 100644 index 0000000000..1130793754 --- /dev/null +++ b/app/client/test/factories/Widgets/ButtonFactory.ts @@ -0,0 +1,31 @@ +import * as Factory from "factory.ts"; +import { generateReactKey } from "utils/generators"; +import { WidgetProps } from "widgets/BaseWidget"; + +export const ButtonFactory = Factory.Sync.makeFactory({ + widgetName: Factory.each((i) => `Button${i + 1}`), + rightColumn: 12, + onClick: "", + isDefaultClickDisabled: true, + widgetId: generateReactKey(), + buttonStyle: "PRIMARY_BUTTON", + topRow: 1, + bottomRow: 2, + parentRowSpace: 38, + isVisible: true, + type: "BUTTON_WIDGET", + dynamicBindingPathList: [], + parentId: "0", + isLoading: false, + parentColumnSpace: 34.6875, + leftColumn: 10, + dynamicTriggerPathList: [ + { + key: "onClick", + }, + ], + text: "Test Button Text", + isDisabled: false, + version: 1, + renderMode: "CANVAS", +}); diff --git a/app/client/test/factories/Widgets/CanvasFactory.ts b/app/client/test/factories/Widgets/CanvasFactory.ts new file mode 100644 index 0000000000..e23c26bca6 --- /dev/null +++ b/app/client/test/factories/Widgets/CanvasFactory.ts @@ -0,0 +1,25 @@ +import * as Factory from "factory.ts"; +import { generateReactKey } from "utils/generators"; +import { WidgetProps } from "widgets/BaseWidget"; + +export const CanvasFactory = Factory.Sync.makeFactory({ + backgroundColor: "none", + rightColumn: 1224, + snapColumns: 16, + detachFromLayout: true, + topRow: 0, + bottomRow: 1280, + containerStyle: "none", + snapRows: 33, + parentRowSpace: 1, + type: "CANVAS_WIDGET", + canExtend: true, + minHeight: 1292, + parentColumnSpace: 1, + dynamicBindingPathList: [], + leftColumn: 0, + widgetName: Factory.each((i) => `Canvas${i + 1}`), + widgetId: generateReactKey(), + renderMode: "CANVAS", + version: 1, +}); diff --git a/app/client/test/factories/Widgets/ChartFactory.ts b/app/client/test/factories/Widgets/ChartFactory.ts new file mode 100644 index 0000000000..2535d90d29 --- /dev/null +++ b/app/client/test/factories/Widgets/ChartFactory.ts @@ -0,0 +1,61 @@ +import * as Factory from "factory.ts"; +import { generateReactKey } from "utils/generators"; +import { WidgetProps } from "widgets/BaseWidget"; + +export const ChartFactory = Factory.Sync.makeFactory({ + isVisible: true, + chartType: "LINE_CHART", + chartName: "Sales on working days", + allowHorizontalScroll: false, + chartData: [ + { + seriesName: "Sales", + data: [ + { + x: "Mon", + y: 10000, + }, + { + x: "Tue", + y: 12000, + }, + { + x: "Wed", + y: 32000, + }, + { + x: "Thu", + y: 28000, + }, + { + x: "Fri", + y: 14000, + }, + { + x: "Sat", + y: 19000, + }, + { + x: "Sun", + y: 36000, + }, + ], + }, + ], + xAxisName: "Last Week", + yAxisName: "Total Order Revenue $", + type: "CHART_WIDGET", + isLoading: false, + parentColumnSpace: 71.75, + parentRowSpace: 38, + leftColumn: 2, + rightColumn: 8, + topRow: 1, + bottomRow: 9, + parentId: "rglduihhzk", + dynamicBindingPathList: [], + widgetName: Factory.each((i) => `Chart${i + 1}`), + widgetId: generateReactKey(), + renderMode: "CANVAS", + version: 1, +}); diff --git a/app/client/test/factories/Widgets/CheckboxFactory.ts b/app/client/test/factories/Widgets/CheckboxFactory.ts new file mode 100644 index 0000000000..2c01e4201f --- /dev/null +++ b/app/client/test/factories/Widgets/CheckboxFactory.ts @@ -0,0 +1,23 @@ +import * as Factory from "factory.ts"; +import { generateReactKey } from "utils/generators"; +import { WidgetProps } from "widgets/BaseWidget"; + +export const CheckboxFactory = Factory.Sync.makeFactory({ + isVisible: true, + label: "Label", + defaultCheckedState: true, + type: "CHECKBOX_WIDGET", + isLoading: false, + parentColumnSpace: 71.75, + parentRowSpace: 38, + leftColumn: 10, + rightColumn: 13, + topRow: 4, + bottomRow: 5, + parentId: "e3tq9qwta6", + dynamicBindingPathList: [], + widgetName: Factory.each((i) => `Checkbox${i + 1}`), + widgetId: generateReactKey(), + renderMode: "CANVAS", + version: 1, +}); diff --git a/app/client/test/factories/Widgets/ContainerFactory.ts b/app/client/test/factories/Widgets/ContainerFactory.ts new file mode 100644 index 0000000000..8abcdc4024 --- /dev/null +++ b/app/client/test/factories/Widgets/ContainerFactory.ts @@ -0,0 +1,25 @@ +import * as Factory from "factory.ts"; +import { generateReactKey } from "utils/generators"; +import { WidgetProps } from "widgets/BaseWidget"; + +export const ContainerFactory = Factory.Sync.makeFactory({ + backgroundColor: "#FFFFFF", + widgetName: Factory.each((i) => `Container${(i+1)}` ), + type: "CONTAINER_WIDGET", + containerStyle: "card", + isVisible: true, + isLoading: false, + parentColumnSpace: 75.25, + parentRowSpace: 38, + dynamicBindingPathList: [], + leftColumn: 0, + rightColumn: 8, + topRow: 0, + bottomRow: 9, + snapColumns: 16, + orientation: "VERTICAL", + children: [], + version: 1, + widgetId: generateReactKey(), + renderMode: "CANVAS", +}); diff --git a/app/client/test/factories/Widgets/DatepickerFactory.ts b/app/client/test/factories/Widgets/DatepickerFactory.ts new file mode 100644 index 0000000000..842bb4c254 --- /dev/null +++ b/app/client/test/factories/Widgets/DatepickerFactory.ts @@ -0,0 +1,47 @@ +import * as Factory from "factory.ts"; +import { generateReactKey } from "utils/generators"; +import { WidgetProps } from "widgets/BaseWidget"; + +export const OldDatepickerFactory = Factory.Sync.makeFactory({ + widgetName: Factory.each((i) => `OldDatePicker${i + 1}`), + rightColumn: 11, + dateFormat: "DD/MM/YYYY", + topRow: 7, + bottomRow: 8, + isValid: "{{ DatePicker1.isRequired ? !!DatePicker1.selectedDate : true }}", + parentRowSpace: 38, + isVisible: true, + datePickerType: "DATE_PICKER", + label: "From Date", + type: "DATE_PICKER_WIDGET", + dynamicBindingPathList: [], + isLoading: false, + enableTimePicker: true, + parentColumnSpace: 34.6875, + leftColumn: 3, + isDisabled: false, + widgetId: generateReactKey(), + renderMode: "CANVAS", + version: 1, +}); + +export const DatepickerFactory = Factory.Sync.makeFactory({ + widgetName: Factory.each((i) => `DatePicker${i + 1}`), + isVisible: true, + isDisabled: false, + datePickerType: "DATE_PICKER", + label: "", + dateFormat: "DD/MM/YYYY HH:mm", + defaultDate: "2021-02-05T10:53:12.791Z", + version: 2, + type: "DATE_PICKER_WIDGET2", + isLoading: false, + parentColumnSpace: 74, + parentRowSpace: 40, + leftColumn: 5, + rightColumn: 10, + topRow: 0, + bottomRow: 1, + widgetId: generateReactKey(), + parentId: "0", +}); diff --git a/app/client/test/factories/Widgets/DropdownFactory.ts b/app/client/test/factories/Widgets/DropdownFactory.ts new file mode 100644 index 0000000000..1ecb47dfc2 --- /dev/null +++ b/app/client/test/factories/Widgets/DropdownFactory.ts @@ -0,0 +1,29 @@ +import * as Factory from "factory.ts"; +import { generateReactKey } from "utils/generators"; +import { WidgetProps } from "widgets/BaseWidget"; + +export const DropdownFactory = Factory.Sync.makeFactory({ + isVisible: true, + label: "", + selectionType: "SINGLE_SELECT", + options: [ + { label: "Vegetarian", value: "VEG" }, + { label: "Non-Vegetarian", value: "NON_VEG" }, + { label: "Vegan", value: "VEGAN" }, + ], + defaultOptionValue: "VEG", + type: "DROP_DOWN_WIDGET", + isLoading: false, + parentColumnSpace: 74, + parentRowSpace: 40, + leftColumn: 10, + rightColumn: 15, + topRow: 1, + bottomRow: 2, + parentId: "0", + widgetId: generateReactKey(), + dynamicBindingPathList: [], + widgetName: Factory.each((i) => `Dropdown${i + 1}`), + renderMode: "CANVAS", + version: 1, +}); diff --git a/app/client/test/factories/Widgets/FilepickerFactory.ts b/app/client/test/factories/Widgets/FilepickerFactory.ts new file mode 100644 index 0000000000..5433db4374 --- /dev/null +++ b/app/client/test/factories/Widgets/FilepickerFactory.ts @@ -0,0 +1,25 @@ +import * as Factory from "factory.ts"; +import { generateReactKey } from "utils/generators"; +import { WidgetProps } from "widgets/BaseWidget"; + +export const FilepickerFactory = Factory.Sync.makeFactory({ + rightColumn: 8, + isDefaultClickDisabled: true, + topRow: 1, + bottomRow: 2, + isValid: "{{ FilePicker1.isRequired ? FilePicker1.files.length > 0 : true }}", + parentRowSpace: 38, + isVisible: true, + label: "Upload Files", + maxFileSize: "", + type: "FILE_PICKER_WIDGET", + dynamicBindingPathList: [], + isLoading: false, + parentColumnSpace: 34.6875, + leftColumn: 4, + files: [], + widgetName: Factory.each((i) => `FilePicker${i + 1}`), + widgetId: generateReactKey(), + renderMode: "CANVAS", + version: 1, +}); diff --git a/app/client/test/factories/Widgets/FormButtonFactory.ts b/app/client/test/factories/Widgets/FormButtonFactory.ts new file mode 100644 index 0000000000..b4ed564115 --- /dev/null +++ b/app/client/test/factories/Widgets/FormButtonFactory.ts @@ -0,0 +1,24 @@ +import * as Factory from "factory.ts"; +import { generateReactKey } from "utils/generators"; +import { WidgetProps } from "widgets/BaseWidget"; + +export const FormButtonFactory = Factory.Sync.makeFactory({ + isVisible: true, + text: "Reset", + isDefaultClickDisabled: true, + buttonStyle: "SECONDARY_BUTTON", + disabledWhenInvalid: false, + resetFormOnClick: true, + type: "FORM_BUTTON_WIDGET", + isLoading: false, + leftColumn: 8, + rightColumn: 12, + topRow: 12, + bottomRow: 13, + parentId: "qrqizehc5b", + dynamicBindingPathList: [], + widgetName: Factory.each((i) => `FormButton${i + 1}`), + widgetId: generateReactKey(), + renderMode: "CANVAS", + version: 1, +}); diff --git a/app/client/test/factories/Widgets/FormFactory.ts b/app/client/test/factories/Widgets/FormFactory.ts new file mode 100644 index 0000000000..9371e44c56 --- /dev/null +++ b/app/client/test/factories/Widgets/FormFactory.ts @@ -0,0 +1,217 @@ +{ +} + +import * as Factory from "factory.ts"; +import { generateReactKey } from "utils/generators"; +import { WidgetProps } from "widgets/BaseWidget"; + +export const FormFactory = Factory.Sync.makeFactory({ + backgroundColor: "Gray", + rightColumn: 11, + topRow: 0, + bottomRow: 13, + parentRowSpace: 38, + isVisible: true, + type: "FORM_WIDGET", + dynamicBindingPathList: [], + blueprint: { + view: [ + { + position: { + top: 0, + left: 0, + }, + type: "CANVAS_WIDGET", + props: { + blueprint: { + view: [ + { + size: { + rows: 1, + cols: 12, + }, + position: { + top: 0, + left: 0, + }, + type: "TEXT_WIDGET", + props: { + text: "Title", + textStyle: "HEADING", + }, + }, + { + size: { + rows: 1, + cols: 4, + }, + position: { + top: 11, + left: 12, + }, + type: "FORM_BUTTON_WIDGET", + props: { + resetFormOnClick: false, + disabledWhenInvalid: true, + buttonStyle: "PRIMARY_BUTTON", + text: "Submit", + }, + }, + { + size: { + rows: 1, + cols: 4, + }, + position: { + top: 11, + left: 8, + }, + type: "FORM_BUTTON_WIDGET", + props: { + resetFormOnClick: true, + disabledWhenInvalid: false, + buttonStyle: "SECONDARY_BUTTON", + text: "Reset", + }, + }, + ], + }, + detachFromLayout: true, + children: [], + containerStyle: "none", + canExtend: false, + }, + }, + ], + }, + isLoading: false, + parentColumnSpace: 71.75, + leftColumn: 5, + children: [ + { + widgetName: "Canvas1", + rightColumn: 430.5, + detachFromLayout: true, + widgetId: "nxlutw2g3v", + containerStyle: "none", + topRow: 0, + bottomRow: 494, + parentRowSpace: 1, + isVisible: true, + canExtend: false, + type: "CANVAS_WIDGET", + dynamicBindingPathList: [], + blueprint: { + view: [ + { + size: { + rows: 1, + cols: 12, + }, + position: { + top: 0, + left: 0, + }, + type: "TEXT_WIDGET", + props: { + text: "Title", + textStyle: "HEADING", + }, + }, + { + size: { + rows: 1, + cols: 4, + }, + position: { + top: 11, + left: 12, + }, + type: "FORM_BUTTON_WIDGET", + props: { + resetFormOnClick: false, + disabledWhenInvalid: true, + buttonStyle: "PRIMARY_BUTTON", + text: "Submit", + }, + }, + { + size: { + rows: 1, + cols: 4, + }, + position: { + top: 11, + left: 8, + }, + type: "FORM_BUTTON_WIDGET", + props: { + resetFormOnClick: true, + disabledWhenInvalid: false, + buttonStyle: "SECONDARY_BUTTON", + text: "Reset", + }, + }, + ], + }, + minHeight: 494, + isLoading: false, + parentColumnSpace: 1, + leftColumn: 0, + children: [ + { + isLoading: false, + widgetName: "Text1", + rightColumn: 12, + leftColumn: 0, + widgetId: "uvz6hzdz7c", + topRow: 0, + bottomRow: 1, + isVisible: true, + text: "Title", + textStyle: "HEADING", + type: "TEXT_WIDGET", + dynamicBindingPathList: [], + }, + { + resetFormOnClick: false, + widgetName: "FormButton1", + rightColumn: 16, + isDefaultClickDisabled: true, + widgetId: "tf20n9k4z2", + buttonStyle: "PRIMARY_BUTTON", + topRow: 11, + bottomRow: 12, + isVisible: true, + type: "FORM_BUTTON_WIDGET", + dynamicBindingPathList: [], + isLoading: false, + disabledWhenInvalid: true, + leftColumn: 12, + text: "Submit", + }, + { + resetFormOnClick: true, + widgetName: "FormButton2", + rightColumn: 12, + isDefaultClickDisabled: true, + widgetId: "6xnpe13jie", + buttonStyle: "SECONDARY_BUTTON", + topRow: 11, + bottomRow: 12, + isVisible: true, + type: "FORM_BUTTON_WIDGET", + dynamicBindingPathList: [], + isLoading: false, + disabledWhenInvalid: false, + leftColumn: 8, + text: "Reset", + }, + ], + }, + ], + widgetName: Factory.each((i) => `Form${i + 1}`), + widgetId: generateReactKey(), + renderMode: "CANVAS", + version: 1, +}); diff --git a/app/client/test/factories/Widgets/IconFactory.ts b/app/client/test/factories/Widgets/IconFactory.ts new file mode 100644 index 0000000000..011cc10264 --- /dev/null +++ b/app/client/test/factories/Widgets/IconFactory.ts @@ -0,0 +1,23 @@ +import * as Factory from "factory.ts"; +import { generateReactKey } from "utils/generators"; +import { WidgetProps } from "widgets/BaseWidget"; + +export const IconFactory = Factory.Sync.makeFactory({ + rightColumn: 16, + onClick: "", + color: "#040627", + iconName: "cross", + topRow: 0, + bottomRow: 1, + isVisible: true, + type: "ICON_WIDGET", + parentId: "dma7flgdrm", + isLoading: false, + leftColumn: 15, + iconSize: 24, + dynamicBindingPathList: [], + widgetName: Factory.each((i) => `Icon${i + 1}`), + widgetId: generateReactKey(), + renderMode: "CANVAS", + version: 1, +}); diff --git a/app/client/test/factories/Widgets/ImageFactory.ts b/app/client/test/factories/Widgets/ImageFactory.ts new file mode 100644 index 0000000000..297ccc684c --- /dev/null +++ b/app/client/test/factories/Widgets/ImageFactory.ts @@ -0,0 +1,24 @@ +import * as Factory from "factory.ts"; +import { generateReactKey } from "utils/generators"; +import { WidgetProps } from "widgets/BaseWidget"; + +export const ImageFactory = Factory.Sync.makeFactory({ + isVisible: true, + defaultImage: + "https://res.cloudinary.com/drako999/image/upload/v1589196259/default.png", + imageShape: "RECTANGLE", + image: "", + widgetName: Factory.each((i) => `Image${i + 1}`), + type: "IMAGE_WIDGET", + isLoading: false, + parentColumnSpace: 34.6875, + parentRowSpace: 38, + leftColumn: 6, + rightColumn: 10, + topRow: 2, + bottomRow: 5, + parentId: "bxekwxgc1i", + widgetId: generateReactKey(), + renderMode: "CANVAS", + version: 1, +}); diff --git a/app/client/test/factories/Widgets/InputFactory.ts b/app/client/test/factories/Widgets/InputFactory.ts new file mode 100644 index 0000000000..ddf4ffd251 --- /dev/null +++ b/app/client/test/factories/Widgets/InputFactory.ts @@ -0,0 +1,30 @@ +import * as Factory from "factory.ts"; +import { generateReactKey } from "utils/generators"; +import { WidgetProps } from "widgets/BaseWidget"; + +export const InputFactory = Factory.Sync.makeFactory({ + widgetName: Factory.each((i) => `Input${i + 1}`), + onTextChanged: "", + rightColumn: 11, + widgetId: generateReactKey(), + topRow: 6, + bottomRow: 7, + isValid: "", + parentRowSpace: 38, + isVisible: true, + label: "Test Input Label", + type: "INPUT_WIDGET", + dynamicBindingPathList: [], + parentId: "iw4o07jvik", + isLoading: false, + parentColumnSpace: 34.6875, + leftColumn: 6, + dynamicTriggerPathList: [ + { + key: "onTextChange", + }, + ], + inputType: "", + placeholderText: "", + defaultText: "", +}); diff --git a/app/client/test/factories/Widgets/ListFactory.ts b/app/client/test/factories/Widgets/ListFactory.ts new file mode 100644 index 0000000000..a91252e06d --- /dev/null +++ b/app/client/test/factories/Widgets/ListFactory.ts @@ -0,0 +1,23 @@ +import * as Factory from "factory.ts"; +import { generateReactKey } from "utils/generators"; +import { WidgetProps } from "widgets/BaseWidget"; + +export const ListFactory = Factory.Sync.makeFactory({ + image: "", + defaultImage: "", + type: "LIST_WIDGET", + parentId: "Container1", + parentColumnSpace: 2, + parentRowSpace: 3, + leftColumn: 2, + rightColumn: 3, + topRow: 1, + bottomRow: 3, + isLoading: false, + items: [], + disablePropertyPane: false, + widgetName: Factory.each((i) => `List${i + 1}`), + widgetId: generateReactKey(), + renderMode: "CANVAS", + version: 1, +}); diff --git a/app/client/test/factories/Widgets/MapFactory.ts b/app/client/test/factories/Widgets/MapFactory.ts new file mode 100644 index 0000000000..23902960e8 --- /dev/null +++ b/app/client/test/factories/Widgets/MapFactory.ts @@ -0,0 +1,32 @@ +import * as Factory from "factory.ts"; +import { generateReactKey } from "utils/generators"; +import { WidgetProps } from "widgets/BaseWidget"; + +export const MapFactory = Factory.Sync.makeFactory({ + isVisible: true, + isDisabled: false, + enableSearch: true, + zoomLevel: 50, + enablePickLocation: true, + allowZoom: true, + mapCenter: { + lat: 20.593684, + long: 78.96288, + }, + defaultMarkers: + '[\n {\n "lat: -34.397,\n "long: 150.644,\n "title: "Test A"\n }\n]', + type: "MAP_WIDGET", + isLoading: false, + parentColumnSpace: 71.75, + parentRowSpace: 38, + leftColumn: 3, + rightColumn: 11, + topRow: 0, + bottomRow: 12, + parentId: "yt4ouwn0sk", + dynamicBindingPathList: [], + widgetName: Factory.each((i) => `Map${i + 1}`), + widgetId: generateReactKey(), + renderMode: "CANVAS", + version: 1, +}); diff --git a/app/client/test/factories/Widgets/ModalFactory.ts b/app/client/test/factories/Widgets/ModalFactory.ts new file mode 100644 index 0000000000..e155a58819 --- /dev/null +++ b/app/client/test/factories/Widgets/ModalFactory.ts @@ -0,0 +1,270 @@ +import * as Factory from "factory.ts"; +import { generateReactKey } from "utils/generators"; +import { WidgetProps } from "widgets/BaseWidget"; + +export const ModalFactory = Factory.Sync.makeFactory({ + rightColumn: 0, + detachFromLayout: true, + topRow: 0, + bottomRow: 0, + parentRowSpace: 1, + isVisible: false, + canOutsideClickClose: true, + type: "MODAL_WIDGET", + canEscapeKeyClose: true, + parentId: "0", + shouldScrollContents: true, + blueprint: { + view: [ + { + position: { + top: 0, + left: 0, + }, + type: "CANVAS_WIDGET", + props: { + shouldScrollContents: false, + blueprint: { + view: [ + { + size: { + rows: 1, + cols: 1, + }, + position: { + top: 0, + left: 15, + }, + type: "ICON_WIDGET", + props: { + color: "#040627", + iconName: "cross", + iconSize: 24, + }, + }, + { + size: { + rows: 1, + cols: 15, + }, + position: { + top: 0, + left: 0, + }, + type: "TEXT_WIDGET", + props: { + text: "Modal Title", + textStyle: "HEADING", + }, + }, + { + size: { + rows: 1, + cols: 3, + }, + position: { + top: 4, + left: 10, + }, + type: "BUTTON_WIDGET", + props: { + buttonStyle: "SECONDARY_BUTTON", + text: "Cancel", + }, + }, + { + size: { + rows: 1, + cols: 3, + }, + position: { + top: 4, + left: 13, + }, + type: "BUTTON_WIDGET", + props: { + buttonStyle: "PRIMARY_BUTTON", + text: "Confirm", + }, + }, + ], + operations: [ + { + type: "MODIFY_PROPS", + }, + ], + }, + detachFromLayout: true, + children: [], + isVisible: true, + isDisabled: false, + canExtend: true, + }, + }, + ], + }, + isLoading: false, + parentColumnSpace: 1, + size: "MODAL_SMALL", + leftColumn: 0, + children: [ + { + widgetName: "Canvas1", + rightColumn: 0, + detachFromLayout: true, + widgetId: "dma7flgdrm", + topRow: 0, + bottomRow: 0, + parentRowSpace: 1, + isVisible: true, + canExtend: true, + type: "CANVAS_WIDGET", + parentId: "s8mtp5krfz", + shouldScrollContents: false, + blueprint: { + view: [ + { + size: { + rows: 1, + cols: 1, + }, + position: { + top: 0, + left: 15, + }, + type: "ICON_WIDGET", + props: { + color: "#040627", + iconName: "cross", + iconSize: 24, + }, + }, + { + size: { + rows: 1, + cols: 15, + }, + position: { + top: 0, + left: 0, + }, + type: "TEXT_WIDGET", + props: { + text: "Modal Title", + textStyle: "HEADING", + }, + }, + { + size: { + rows: 1, + cols: 3, + }, + position: { + top: 4, + left: 10, + }, + type: "BUTTON_WIDGET", + props: { + buttonStyle: "SECONDARY_BUTTON", + text: "Cancel", + }, + }, + { + size: { + rows: 1, + cols: 3, + }, + position: { + top: 4, + left: 13, + }, + type: "BUTTON_WIDGET", + props: { + buttonStyle: "PRIMARY_BUTTON", + text: "Confirm", + }, + }, + ], + operations: [ + { + type: "MODIFY_PROPS", + }, + ], + }, + minHeight: 0, + isLoading: false, + parentColumnSpace: 1, + leftColumn: 0, + children: [ + { + widgetName: "Icon1", + rightColumn: 16, + onClick: "{{closeModal('TestModal')}}", + color: "#040627", + iconName: "cross", + widgetId: "n5fc0ven2a", + topRow: 0, + bottomRow: 1, + isVisible: true, + type: "ICON_WIDGET", + parentId: "dma7flgdrm", + isLoading: false, + leftColumn: 15, + iconSize: 24, + }, + { + isLoading: false, + widgetName: "Text1", + rightColumn: 15, + leftColumn: 0, + widgetId: "s818l5hhvq", + topRow: 0, + bottomRow: 1, + isVisible: true, + text: "Modal Title", + textStyle: "HEADING", + type: "TEXT_WIDGET", + parentId: "dma7flgdrm", + }, + { + widgetName: "Button1", + rightColumn: 13, + isDefaultClickDisabled: true, + widgetId: "io777lh7bc", + buttonStyle: "SECONDARY_BUTTON", + topRow: 4, + bottomRow: 5, + isVisible: true, + type: "BUTTON_WIDGET", + parentId: "dma7flgdrm", + isLoading: false, + leftColumn: 10, + text: "Cancel", + isDisabled: false, + }, + { + widgetName: "Button2", + rightColumn: 16, + isDefaultClickDisabled: true, + widgetId: "gexp49bfyb", + buttonStyle: "PRIMARY_BUTTON", + topRow: 4, + bottomRow: 5, + isVisible: true, + type: "BUTTON_WIDGET", + parentId: "dma7flgdrm", + isLoading: false, + leftColumn: 13, + text: "Confirm", + isDisabled: false, + }, + ], + isDisabled: false, + }, + ], + dynamicBindingPathList: [], + widgetName: Factory.each((i) => `Modal${i + 1}`), + widgetId: generateReactKey(), + renderMode: "CANVAS", + version: 1, +}); diff --git a/app/client/test/factories/Widgets/RadiogroupFactory.ts b/app/client/test/factories/Widgets/RadiogroupFactory.ts new file mode 100644 index 0000000000..f34c5a3335 --- /dev/null +++ b/app/client/test/factories/Widgets/RadiogroupFactory.ts @@ -0,0 +1,42 @@ +import * as Factory from "factory.ts"; +import { generateReactKey } from "utils/generators"; +import { WidgetProps } from "widgets/BaseWidget"; + +export const RadiogroupFactory = Factory.Sync.makeFactory({ + rightColumn: 16, + topRow: 3, + bottomRow: 5, + parentRowSpace: 38, + isVisible: true, + label: "Test Radio", + type: "RADIO_GROUP_WIDGET", + isLoading: false, + defaultOptionValue: "1", + parentColumnSpace: 34.6875, + leftColumn: 12, + dynamicTriggerPathList: [{ + key: "onSelectionChange" + }], + onSelectionChange: "{{navigateTo()}}", + options: [ + { + id: "1", + label: "jarvis", + value: "1" + }, + { + id: "2", + label: "marvel", + value: "2" + }, + { + label: "iron", + value: "4" + } + ], + dynamicBindingPathList: [], + widgetName: Factory.each((i) => `RadioGroup${i + 1}`), + widgetId: generateReactKey(), + renderMode: "CANVAS", + version: 1, +}); diff --git a/app/client/test/factories/Widgets/RichTextFactory.ts b/app/client/test/factories/Widgets/RichTextFactory.ts new file mode 100644 index 0000000000..d8bf8cd055 --- /dev/null +++ b/app/client/test/factories/Widgets/RichTextFactory.ts @@ -0,0 +1,29 @@ +import * as Factory from "factory.ts"; +import { generateReactKey } from "utils/generators"; +import { WidgetProps } from "widgets/BaseWidget"; + +export const RichTextFactory = Factory.Sync.makeFactory({ + rightColumn: 11, + topRow: 3, + bottomRow: 8, + parentRowSpace: 38, + isVisible: true, + type: "RICH_TEXT_EDITOR_WIDGET", + isLoading: false, + parentColumnSpace: 34.6875, + leftColumn: 3, + dynamicTriggerPathList: [ + { + key: "onTextChange", + }, + ], + defaultText: "", + text: "This is the initial content of the editor", + isDisabled: false, + onTextChange: "{{navigateTo()}}", + dynamicBindingPathList: [], + widgetName: Factory.each((i) => `RichText${i + 1}`), + widgetId: generateReactKey(), + renderMode: "CANVAS", + version: 1, +}); diff --git a/app/client/test/factories/Widgets/SkeletonFactory.ts b/app/client/test/factories/Widgets/SkeletonFactory.ts new file mode 100644 index 0000000000..06ba9011ce --- /dev/null +++ b/app/client/test/factories/Widgets/SkeletonFactory.ts @@ -0,0 +1,19 @@ +import * as Factory from "factory.ts"; +import { generateReactKey } from "utils/generators"; +import { WidgetProps } from "widgets/BaseWidget"; + +export const SkeletonFactory = Factory.Sync.makeFactory({ + bottomRow: 0, + isLoading: false, + leftColumn: 0, + parentColumnSpace: 0, + parentRowSpace: 0, + rightColumn: 0, + topRow: 0, + type: "SKELETON_WIDGET", + dynamicBindingPathList: [], + widgetName: Factory.each((i) => `Skeleton${i + 1}`), + widgetId: generateReactKey(), + renderMode: "CANVAS", + version: 1, +}); diff --git a/app/client/test/factories/Widgets/SwitchFactory.ts b/app/client/test/factories/Widgets/SwitchFactory.ts new file mode 100644 index 0000000000..1695691444 --- /dev/null +++ b/app/client/test/factories/Widgets/SwitchFactory.ts @@ -0,0 +1,22 @@ +import * as Factory from "factory.ts"; +import { generateReactKey } from "utils/generators"; +import { WidgetProps } from "widgets/BaseWidget"; + +export const SwitchFactory = Factory.Sync.makeFactory({ + isVisible: true, + label: "Switch", + defaultSwitchState: true, + widgetName: Factory.each((i) => `Switch${(i+1)}` ), + type: "SWITCH_WIDGET", + isLoading: false, + parentColumnSpace: 71.75, + parentRowSpace: 38, + leftColumn: 10, + rightColumn: 13, + topRow: 18, + bottomRow: 19, + parentId: "e3tq9qwta6", + widgetId: generateReactKey(), + renderMode: "CANVAS", + version: 1, +}); diff --git a/app/client/test/factories/Widgets/TableFactory.ts b/app/client/test/factories/Widgets/TableFactory.ts new file mode 100644 index 0000000000..d694fd3fbd --- /dev/null +++ b/app/client/test/factories/Widgets/TableFactory.ts @@ -0,0 +1,111 @@ +import * as Factory from "factory.ts"; +import { generateReactKey } from "utils/generators"; +import { WidgetProps } from "widgets/BaseWidget"; + +export const TableFactory = Factory.Sync.makeFactory({ + isVisible: true, + label: "Data", + widgetName: Factory.each((i) => `Table${i + 1}`), + + searchKey: "", + tableData: + '[\n {\n "id: 2381224,\n "email: "michael.lawson@reqres.in",\n "userName: "Michael Lawson",\n "productName: "Chicken Sandwich",\n "orderAmount: 4.99\n },\n {\n "id: 2736212,\n "email: "lindsay.ferguson@reqres.in",\n "userName: "Lindsay Ferguson",\n "productName: "Tuna Salad",\n "orderAmount: 9.99\n },\n {\n "id: 6788734,\n "email: "tobias.funke@reqres.in",\n "userName: "Tobias Funke",\n "productName: "Beef steak",\n "orderAmount: 19.99\n }\n]', + type: "TABLE_WIDGET", + isLoading: false, + parentColumnSpace: 74, + parentRowSpace: 40, + leftColumn: 2, + rightColumn: 10, + topRow: 12, + bottomRow: 19, + parentId: "0", + widgetId: generateReactKey(), + dynamicBindingPathList: [], + primaryColumns: { + id: { + index: 0, + width: 150, + id: "id", + horizontalAlignment: "LEFT", + verticalAlignment: "CENTER", + columnType: "text", + textColor: "#4E5D78", + textSize: "PARAGRAPH", + fontStyle: "NORMAL", + enableFilter: true, + enableSort: true, + isVisible: true, + isDerived: false, + label: "id", + computedValue: "", + }, + email: { + index: 1, + width: 150, + id: "email", + horizontalAlignment: "LEFT", + verticalAlignment: "CENTER", + columnType: "text", + textColor: "#4E5D78", + textSize: "PARAGRAPH", + fontStyle: "NORMAL", + enableFilter: true, + enableSort: true, + isVisible: true, + isDerived: false, + label: "email", + computedValue: "", + }, + userName: { + index: 2, + width: 150, + id: "userName", + horizontalAlignment: "LEFT", + verticalAlignment: "CENTER", + columnType: "text", + textColor: "#4E5D78", + textSize: "PARAGRAPH", + fontStyle: "NORMAL", + enableFilter: true, + enableSort: true, + isVisible: true, + isDerived: false, + label: "userName", + computedValue: "", + }, + productName: { + index: 3, + width: 150, + id: "productName", + horizontalAlignment: "LEFT", + verticalAlignment: "CENTER", + columnType: "text", + textColor: "#4E5D78", + textSize: "PARAGRAPH", + fontStyle: "NORMAL", + enableFilter: true, + enableSort: true, + isVisible: true, + isDerived: false, + label: "productName", + computedValue: "", + }, + orderAmount: { + index: 4, + width: 150, + id: "orderAmount", + horizontalAlignment: "LEFT", + verticalAlignment: "CENTER", + columnType: "text", + textColor: "#4E5D78", + textSize: "PARAGRAPH", + fontStyle: "NORMAL", + enableFilter: true, + enableSort: true, + isVisible: true, + isDerived: false, + label: "orderAmount", + computedValue: "", + }, + }, +}); diff --git a/app/client/test/factories/Widgets/TabsFactory.ts b/app/client/test/factories/Widgets/TabsFactory.ts new file mode 100644 index 0000000000..d948f39c49 --- /dev/null +++ b/app/client/test/factories/Widgets/TabsFactory.ts @@ -0,0 +1,145 @@ +import * as Factory from "factory.ts"; +import { generateReactKey } from "utils/generators"; +import { WidgetProps } from "widgets/BaseWidget"; + +export const OldTabsFactory = Factory.Sync.makeFactory({ + isVisible: true, + shouldScrollContents: false, + tabs: [ + { + label: "Tab 1", + id: "tab1", + widgetId: "o9ody00ep7", + }, + { + label: "Tab 2", + id: "tab2", + widgetId: "plhuaxd4lo", + }, + ], + shouldShowTabs: true, + defaultTab: "Tab 1", + type: "TABS_WIDGET", + isLoading: false, + parentColumnSpace: 74, + parentRowSpace: 40, + leftColumn: 1, + rightColumn: 9, + topRow: 12, + bottomRow: 19, + parentId: "0", + children: [ + { + type: "CANVAS_WIDGET", + tabId: "tab1", + tabName: "Tab 1", + widgetId: "o9ody00ep7", + parentId: "jd83uvbkmp", + detachFromLayout: true, + children: [], + parentRowSpace: 1, + parentColumnSpace: 1, + leftColumn: 0, + rightColumn: 592, + topRow: 0, + bottomRow: 280, + isLoading: false, + widgetName: "Canvas1", + renderMode: "CANVAS", + }, + { + type: "CANVAS_WIDGET", + tabId: "tab2", + tabName: "Tab 2", + widgetId: "plhuaxd4lo", + parentId: "jd83uvbkmp", + detachFromLayout: true, + children: [], + parentRowSpace: 1, + parentColumnSpace: 1, + leftColumn: 0, + rightColumn: 592, + topRow: 0, + bottomRow: 280, + isLoading: false, + widgetName: "Canvas1", + renderMode: "CANVAS", + }, + ], + dynamicBindingPathList: [], + widgetName: Factory.each((i) => `Tabs${i + 1}`), + widgetId: generateReactKey(), + renderMode: "CANVAS", + version: 1, +}); + +export const TabsFactory = Factory.Sync.makeFactory({ + isVisible: true, + shouldScrollContents: false, + tabsObj: { + tab1: { + label: "Tab 1", + id: "tab1", + widgetId: "o9ody00ep7", + }, + tab2: { + label: "Tab 2", + id: "tab2", + widgetId: "plhuaxd4lo", + }, + }, + shouldShowTabs: true, + defaultTab: "Tab 1", + type: "TABS_WIDGET", + isLoading: false, + parentColumnSpace: 74, + parentRowSpace: 40, + leftColumn: 1, + rightColumn: 9, + topRow: 12, + bottomRow: 19, + parentId: "0", + children: [ + { + type: "CANVAS_WIDGET", + tabId: "tab1", + tabName: "Tab 1", + widgetId: "o9ody00ep7", + parentId: "jd83uvbkmp", + detachFromLayout: true, + children: [], + parentRowSpace: 1, + parentColumnSpace: 1, + leftColumn: 0, + rightColumn: 592, + topRow: 0, + bottomRow: 280, + isLoading: false, + widgetName: "Canvas1", + renderMode: "CANVAS", + }, + { + type: "CANVAS_WIDGET", + tabId: "tab2", + tabName: "Tab 2", + widgetId: "plhuaxd4lo", + parentId: "jd83uvbkmp", + detachFromLayout: true, + children: [], + parentRowSpace: 1, + parentColumnSpace: 1, + leftColumn: 0, + rightColumn: 592, + topRow: 0, + bottomRow: 280, + isLoading: false, + widgetName: "Canvas1", + renderMode: "CANVAS", + }, + ], + dynamicBindingPathList: [], + widgetName: Factory.each((i) => `Tabs${i + 1}`), + widgetId: generateReactKey(), + renderMode: "CANVAS", + version: 1, +}); diff --git a/app/client/test/factories/Widgets/TextFactory.ts b/app/client/test/factories/Widgets/TextFactory.ts new file mode 100644 index 0000000000..e0fd5e9bb1 --- /dev/null +++ b/app/client/test/factories/Widgets/TextFactory.ts @@ -0,0 +1,31 @@ +import * as Factory from "factory.ts"; +import { generateReactKey } from "utils/generators"; +import { WidgetProps } from "widgets/BaseWidget"; + +export const TextFactory = Factory.Sync.makeFactory({ + widgetName: Factory.each((i) => `Text${(i+1)}`), + rightColumn: 12, + onClick: "", + isDefaultClickDisabled: true, + widgetId: generateReactKey(), + buttonStyle: "PRIMARY_BUTTON", + topRow: 1, + bottomRow: 2, + parentRowSpace: 38, + isVisible: true, + type: "BUTTON_WIDGET", + dynamicBindingPathList: [], + parentId: "0", + isLoading: false, + parentColumnSpace: 34.6875, + leftColumn: 10, + dynamicTriggerPathList: [ + { + key: "onClick", + }, + ], + text: "Test Button Text", + isDisabled: false, + version: 1, + renderMode: "CANVAS", +}); diff --git a/app/client/test/factories/Widgets/VideoFactory.ts b/app/client/test/factories/Widgets/VideoFactory.ts new file mode 100644 index 0000000000..d0662eada2 --- /dev/null +++ b/app/client/test/factories/Widgets/VideoFactory.ts @@ -0,0 +1,23 @@ +import * as Factory from "factory.ts"; +import { generateReactKey } from "utils/generators"; +import { WidgetProps } from "widgets/BaseWidget"; + +export const VideoFactory = Factory.Sync.makeFactory({ + isVisible: true, + url: "https://www.youtube.com/watch?v=mzqK0QIZRLs", + autoPlay: false, + type: "VIDEO_WIDGET", + isLoading: false, + parentColumnSpace: 74, + parentRowSpace: 40, + leftColumn: 1, + rightColumn: 8, + topRow: 17, + bottomRow: 24, + parentId: "0", + dynamicBindingPathList: [], + widgetName: Factory.each((i) => `Video${i + 1}`), + widgetId: generateReactKey(), + renderMode: "CANVAS", + version: 1, +}); diff --git a/app/client/test/factories/Widgets/WidgetTypeFactories.ts b/app/client/test/factories/Widgets/WidgetTypeFactories.ts new file mode 100644 index 0000000000..0b7a142f0e --- /dev/null +++ b/app/client/test/factories/Widgets/WidgetTypeFactories.ts @@ -0,0 +1,53 @@ +import { SwitchFactory } from "./SwitchFactory"; +import { ButtonFactory } from "./ButtonFactory"; +import { TextFactory } from "./TextFactory"; +import { ImageFactory } from "./ImageFactory"; +import { InputFactory } from "./InputFactory"; +import { TableFactory } from "./TableFactory"; +import { OldDatepickerFactory, DatepickerFactory } from "./DatepickerFactory"; +import { ContainerFactory } from "./ContainerFactory"; +import { DropdownFactory } from "./DropdownFactory"; +import { CheckboxFactory } from "./CheckboxFactory"; +import { RadiogroupFactory } from "./RadiogroupFactory"; +import { OldTabsFactory, TabsFactory } from "./TabsFactory"; +import { ModalFactory } from "./ModalFactory"; +import { RichTextFactory } from "./RichTextFactory"; +import { ChartFactory } from "./ChartFactory"; +import { FormFactory } from "./FormFactory"; +import { FormButtonFactory } from "./FormButtonFactory"; +import { MapFactory } from "./MapFactory"; +import { CanvasFactory } from "./CanvasFactory"; +import { IconFactory } from "./IconFactory"; +import { FilepickerFactory } from "./FilepickerFactory"; +import { VideoFactory } from "./VideoFactory"; +import { SkeletonFactory } from "./SkeletonFactory"; +import { ListFactory } from "./ListFactory"; + +export const WidgetTypeFactories = { + SWITCH_WIDGET: SwitchFactory, + BUTTON_WIDGET: ButtonFactory, + TEXT_WIDGET: TextFactory, + IMAGE_WIDGET: ImageFactory, + INPUT_WIDGET: InputFactory, + CONTAINER_WIDGET: ContainerFactory, + DATE_PICKER_WIDGET: OldDatepickerFactory, + DATE_PICKER_WIDGET2: DatepickerFactory, + TABLE_WIDGET: TableFactory, + DROP_DOWN_WIDGET: DropdownFactory, + CHECKBOX_WIDGET: CheckboxFactory, + RADIO_GROUP_WIDGET: RadiogroupFactory, + TABS_WIDGET: TabsFactory, + TABS_MIGRATOR_WIDGET: OldTabsFactory, + MODAL_WIDGET: ModalFactory, + RICH_TEXT_EDITOR_WIDGET: RichTextFactory, + CHART_WIDGET: ChartFactory, + FORM_WIDGET: FormFactory, + FORM_BUTTON_WIDGET: FormButtonFactory, + MAP_WIDGET: MapFactory, + CANVAS_WIDGET: CanvasFactory, + ICON_WIDGET: IconFactory, + FILE_PICKER_WIDGET: FilepickerFactory, + VIDEO_WIDGET: VideoFactory, + SKELETON_WIDGET: SkeletonFactory, + LIST_WIDGET: ListFactory, +}; diff --git a/app/client/test/testUtils.tsx b/app/client/test/testUtils.tsx index 2fe17dee60..11f9f623f7 100644 --- a/app/client/test/testUtils.tsx +++ b/app/client/test/testUtils.tsx @@ -7,6 +7,8 @@ import { getCurrentThemeDetails } from "../src/selectors/themeSelectors"; import * as customQueries from "./customQueries"; import { BrowserRouter } from "react-router-dom"; import { AppState } from "reducers"; +import { DndProvider } from "react-dnd"; +import TouchBackend from "react-dnd-touch-backend"; const customRender = ( ui: ReactElement, @@ -25,7 +27,14 @@ const customRender = ( return render( - {ui} + + {ui} + , { diff --git a/app/client/yarn.lock b/app/client/yarn.lock index 62dc274e41..214eb22901 100644 --- a/app/client/yarn.lock +++ b/app/client/yarn.lock @@ -8162,6 +8162,14 @@ extsprintf@^1.2.0: version "1.4.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" +factory.ts@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/factory.ts/-/factory.ts-0.5.1.tgz#4bab72d8457078906aa6ab396c0d341e8a3ab382" + integrity sha512-jwAq8w7MmxUojIFzKezMwTzDc5QoxcqzAA8+n9A0EAWBje2CRHUeBrW9x/ioV2DRjHgkHX7i0G0ipfDhlatIQw== + dependencies: + clone-deep "^4.0.1" + source-map-support "^0.5.9" + fast-deep-equal@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" @@ -15637,7 +15645,7 @@ source-map-resolve@^0.6.0: atob "^2.1.2" decode-uri-component "^0.2.0" -source-map-support@^0.5.16, source-map-support@^0.5.6, source-map-support@~0.5.12, source-map-support@~0.5.19: +source-map-support@^0.5.16, source-map-support@^0.5.6, source-map-support@^0.5.9, source-map-support@~0.5.12, source-map-support@~0.5.19: version "0.5.19" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" dependencies: