diff --git a/app/client/src/components/propertyControls/MenuButtonDynamicItemsControl.tsx b/app/client/src/components/propertyControls/MenuButtonDynamicItemsControl.tsx index b76b5e893b..4e941bc0d8 100644 --- a/app/client/src/components/propertyControls/MenuButtonDynamicItemsControl.tsx +++ b/app/client/src/components/propertyControls/MenuButtonDynamicItemsControl.tsx @@ -18,6 +18,8 @@ import { stringToJS, } from "components/editorComponents/ActionCreator/utils"; import { AdditionalDynamicDataTree } from "utils/autocomplete/customTreeTypeDefCreator"; +import { ColumnProperties } from "widgets/TableWidgetV2/component/Constants"; +import { getUniqueKeysFromSourceData } from "widgets/MenuButtonWidget/widget/helper"; const PromptMessage = styled.span` line-height: 17px; @@ -93,18 +95,34 @@ class MenuButtonDynamicItemsControl extends BaseControl< label, propertyValue, theme, + widgetProperties, } = this.props; - const menuButtonId = this.props.widgetProperties.widgetName; + const widgetName = widgetProperties.widgetName; + const widgetType = widgetProperties.type; const value = propertyValue && isDynamicValue(propertyValue) ? MenuButtonDynamicItemsControl.getInputComputedValue( propertyValue, - menuButtonId, + widgetName, + widgetType, + widgetProperties.primaryColumns, ) : propertyValue ? propertyValue : defaultValue; - const keys = this.props.widgetProperties.sourceDataKeys || []; + let sourceData; + + if (widgetType === "TABLE_WIDGET_V2") { + sourceData = + widgetProperties?.__evaluation__?.evaluatedValues?.primaryColumns?.[ + `${Object.keys(widgetProperties.primaryColumns)[0]}` + ]?.sourceData; + } else if (widgetType === "MENU_BUTTON_WIDGET") { + sourceData = + widgetProperties?.__evaluation__?.evaluatedValues?.sourceData; + } + + const keys = getUniqueKeysFromSourceData(sourceData); const currentItem: { [key: string]: any } = {}; Object.values(keys).forEach((key) => { @@ -115,6 +133,7 @@ class MenuButtonDynamicItemsControl extends BaseControl< if (value && !propertyValue) { this.onTextChange(value); } + return ( { - return `{{${menuButtonId}.sourceData.map((currentItem, currentIndex) => ( `; + static getBindingPrefix = ( + widgetName: string, + widgetType?: string, + primaryColumns?: Record, + ) => { + if (widgetType === "TABLE_WIDGET_V2" && primaryColumns) { + const columnName = Object.keys(primaryColumns)?.[0]; + + return `{{${widgetName}.processedTableData.map((currentRow, currentRowIndex) => { + let primaryColumnData = []; + + if (${widgetName}.primaryColumns.${columnName}.sourceData[currentRowIndex].length) { + primaryColumnData = ${widgetName}.primaryColumns.${columnName}.sourceData[currentRowIndex]; + } else if (${widgetName}.primaryColumns.${columnName}.sourceData.length) { + primaryColumnData = ${widgetName}.primaryColumns.${columnName}.sourceData; + } + + return primaryColumnData.map((currentItem, currentIndex) => `; + } else { + return `{{${widgetName}.sourceData.map((currentItem, currentIndex) => ( `; + } }; - static bindingSuffix = `))}}`; + static getBindingSuffix = (widgetType?: string) => + widgetType === "TABLE_WIDGET_V2" ? `);});}}` : `))}}`; static getInputComputedValue = ( propertyValue: string, - menuButtonId: string, + widgetName: string, + widgetType?: string, + primaryColumns?: Record, ) => { - if (!propertyValue.includes(this.getBindingPrefix(menuButtonId))) { + if ( + !propertyValue.includes( + this.getBindingPrefix(widgetName, widgetType, primaryColumns), + ) + ) { return propertyValue; } const value = `${propertyValue.substring( - this.getBindingPrefix(menuButtonId).length, - propertyValue.length - this.bindingSuffix.length, + this.getBindingPrefix(widgetName, widgetType, primaryColumns).length, + propertyValue.length - this.getBindingSuffix(widgetType).length, )}`; const stringValue = JSToString(value); return stringValue; }; - getComputedValue = (value: string, menuButtonId: string) => { + getComputedValue = ( + value: string, + widgetName: string, + widgetType?: string, + primaryColumns?: Record, + ) => { if (!isDynamicValue(value)) { return value; } @@ -166,8 +216,12 @@ class MenuButtonDynamicItemsControl extends BaseControl< } return `${MenuButtonDynamicItemsControl.getBindingPrefix( - menuButtonId, - )}${stringToEvaluate}${MenuButtonDynamicItemsControl.bindingSuffix}`; + widgetName, + widgetType, + primaryColumns, + )}${stringToEvaluate}${MenuButtonDynamicItemsControl.getBindingSuffix( + widgetType, + )}`; }; onTextChange = (event: React.ChangeEvent | string) => { @@ -181,6 +235,8 @@ class MenuButtonDynamicItemsControl extends BaseControl< const output = this.getComputedValue( value, this.props.widgetProperties.widgetName, + this.props.widgetProperties.type, + this.props.widgetProperties.primaryColumns, ); this.updateProperty(this.props.propertyName, output); diff --git a/app/client/src/components/propertyControls/TableComputeValue.tsx b/app/client/src/components/propertyControls/TableComputeValue.tsx index 0451c2beed..4088ab8939 100644 --- a/app/client/src/components/propertyControls/TableComputeValue.tsx +++ b/app/client/src/components/propertyControls/TableComputeValue.tsx @@ -177,11 +177,13 @@ class ComputeTablePropertyControlV2 extends BaseControl< onTextChange = (event: React.ChangeEvent | string) => { let value = ""; + if (typeof event !== "string") { value = event.target?.value; } else { value = event; } + if (isString(value)) { const output = this.getComputedValue( value, diff --git a/app/client/src/constants/WidgetConstants.tsx b/app/client/src/constants/WidgetConstants.tsx index bb2888ff07..b3b2173532 100644 --- a/app/client/src/constants/WidgetConstants.tsx +++ b/app/client/src/constants/WidgetConstants.tsx @@ -70,7 +70,7 @@ export const layoutConfigurations: LayoutConfigurations = { FLUID: { minWidth: -1, maxWidth: -1 }, }; -export const LATEST_PAGE_VERSION = 74; +export const LATEST_PAGE_VERSION = 75; export const GridDefaults = { DEFAULT_CELL_SIZE: 1, diff --git a/app/client/src/utils/DSLMigration.test.ts b/app/client/src/utils/DSLMigration.test.ts index a5d93d4a99..9a72ec66b8 100644 --- a/app/client/src/utils/DSLMigration.test.ts +++ b/app/client/src/utils/DSLMigration.test.ts @@ -718,6 +718,15 @@ const migrations: Migration[] = [ ], version: 73, }, + { + functionLookup: [ + { + moduleObj: tableMigrations, + functionName: "migrateMenuButtonDynamicItemsInsideTableWidget", + }, + ], + version: 74, + }, ]; const mockFnObj: Record = {}; diff --git a/app/client/src/utils/DSLMigrations.ts b/app/client/src/utils/DSLMigrations.ts index 290c1037e7..d80d541896 100644 --- a/app/client/src/utils/DSLMigrations.ts +++ b/app/client/src/utils/DSLMigrations.ts @@ -23,6 +23,7 @@ import { migrateTableWidgetIconButtonVariant, migrateTableWidgetV2Validation, migrateTableWidgetV2ValidationBinding, + migrateMenuButtonDynamicItemsInsideTableWidget, migrateTableWidgetV2SelectOption, } from "./migrations/TableWidget"; import { @@ -1155,6 +1156,11 @@ export const transformDSL = (currentDSL: DSLWidget, newPage = false) => { if (currentDSL.version === 73) { currentDSL = migrateInputWidgetShowStepArrows(currentDSL); + currentDSL.version = 74; + } + + if (currentDSL.version === 74) { + currentDSL = migrateMenuButtonDynamicItemsInsideTableWidget(currentDSL); currentDSL.version = LATEST_PAGE_VERSION; } diff --git a/app/client/src/utils/migrations/TableWidget.ts b/app/client/src/utils/migrations/TableWidget.ts index 73a2363177..1f8b0faa68 100644 --- a/app/client/src/utils/migrations/TableWidget.ts +++ b/app/client/src/utils/migrations/TableWidget.ts @@ -670,3 +670,25 @@ export const migrateTableWidgetV2SelectOption = (currentDSL: DSLWidget) => { } }); }; + +export const migrateMenuButtonDynamicItemsInsideTableWidget = ( + currentDSL: DSLWidget, +) => { + return traverseDSLAndMigrate(currentDSL, (widget: WidgetProps) => { + if (widget.type === "TABLE_WIDGET_V2") { + const primaryColumns = widget.primaryColumns; + + if (primaryColumns) { + for (const column in primaryColumns) { + if ( + primaryColumns.hasOwnProperty(column) && + primaryColumns[column].columnType === "menuButton" && + !primaryColumns[column].menuItemsSource + ) { + primaryColumns[column].menuItemsSource = "STATIC"; + } + } + } + } + }); +}; diff --git a/app/client/src/widgets/MenuButtonWidget/constants.ts b/app/client/src/widgets/MenuButtonWidget/constants.ts index 8393fdf818..a5eacc7ded 100644 --- a/app/client/src/widgets/MenuButtonWidget/constants.ts +++ b/app/client/src/widgets/MenuButtonWidget/constants.ts @@ -34,12 +34,14 @@ export interface ConfigureMenuItems { config: MenuItem; } +export type MenuItems = Record; + export interface MenuButtonWidgetProps extends WidgetProps { label?: string; isDisabled?: boolean; isVisible?: boolean; isCompact?: boolean; - menuItems: Record; + menuItems: MenuItems; getVisibleItems: () => Array; menuVariant?: ButtonVariant; menuColor?: string; @@ -51,7 +53,6 @@ export interface MenuButtonWidgetProps extends WidgetProps { menuItemsSource: MenuItemsSource; configureMenuItems: ConfigureMenuItems; sourceData?: Array>; - sourceDataKeys?: Array; } export interface MenuButtonComponentProps { @@ -59,7 +60,7 @@ export interface MenuButtonComponentProps { isDisabled?: boolean; isVisible?: boolean; isCompact?: boolean; - menuItems: Record; + menuItems: MenuItems; getVisibleItems: () => Array; menuVariant?: ButtonVariant; menuColor?: string; @@ -77,11 +78,10 @@ export interface MenuButtonComponentProps { menuItemsSource: MenuItemsSource; configureMenuItems: ConfigureMenuItems; sourceData?: Array>; - sourceDataKeys?: Array; } export interface PopoverContentProps { - menuItems: Record; + menuItems: MenuItems; getVisibleItems: () => Array; onItemClicked: (onClick: string | undefined, index: number) => void; isCompact?: boolean; @@ -90,7 +90,6 @@ export interface PopoverContentProps { menuItemsSource: MenuItemsSource; configureMenuItems: ConfigureMenuItems; sourceData?: Array>; - sourceDataKeys?: Array; } export const ICON_NAMES = Object.keys(IconNames).map( diff --git a/app/client/src/widgets/MenuButtonWidget/validations.ts b/app/client/src/widgets/MenuButtonWidget/validations.ts index 1baabee778..af0b14a5f9 100644 --- a/app/client/src/widgets/MenuButtonWidget/validations.ts +++ b/app/client/src/widgets/MenuButtonWidget/validations.ts @@ -1,3 +1,4 @@ +import { ValidationConfig } from "constants/PropertyControlConstants"; import { ValidationResponse } from "constants/WidgetValidation"; import { MenuButtonWidgetProps } from "./constants"; @@ -43,3 +44,253 @@ export function sourceDataArrayValidation( return invalidResponse; } } + +export function textForEachRowValidation( + value: unknown, + props: MenuButtonWidgetProps, + _: any, +): ValidationResponse { + const generateResponseAndReturn = (isValid = false, message = "") => { + return { + isValid, + parsed: isValid ? value : [], + messages: [message], + }; + }; + + const DEFAULT_MESSAGE = + "The evaluated value should be either a string or a number."; + + if ( + _.isString(value) || + _.isNumber(value) || + Array.isArray(value) || + value === undefined + ) { + if (Array.isArray(value)) { + const isValid = value.every((item) => { + if (_.isString(item) || _.isNumber(item) || item === undefined) { + return true; + } + + if (Array.isArray(item)) { + return item.every( + (subItem) => + _.isString(subItem) || + _.isNumber(subItem) || + subItem === undefined, + ); + } + + return false; + }); + + return isValid + ? generateResponseAndReturn(true) + : generateResponseAndReturn(false, DEFAULT_MESSAGE); + } + + return generateResponseAndReturn(true); + } + + return generateResponseAndReturn(false, DEFAULT_MESSAGE); +} + +export function booleanForEachRowValidation( + value: unknown, +): ValidationResponse { + const generateResponseAndReturn = (isValid = false, message = "") => { + return { + isValid, + parsed: isValid ? value : true, + messages: [message], + }; + }; + + const isBoolean = (value: unknown) => { + const isABoolean = value === true || value === false; + const isStringTrueFalse = value === "true" || value === "false"; + + return isABoolean || isStringTrueFalse || value === undefined; + }; + + const DEFAULT_MESSAGE = "The evaluated value should be a boolean."; + + if (isBoolean(value)) { + return generateResponseAndReturn(true); + } + + if (Array.isArray(value)) { + const isValid = value.every((item) => { + if (isBoolean(item)) { + return true; + } + + if (Array.isArray(item)) { + return item.every((subItem) => isBoolean(subItem)); + } + + return false; + }); + + return isValid + ? generateResponseAndReturn(true) + : generateResponseAndReturn(false, DEFAULT_MESSAGE); + } + + return generateResponseAndReturn(false, DEFAULT_MESSAGE); +} + +export function iconNamesForEachRowValidation( + value: unknown, + props: MenuButtonWidgetProps, + _: any, + moment: any, + propertyPath: string, + config: ValidationConfig, +): ValidationResponse { + const generateResponseAndReturn = (isValid = false, message = "") => { + return { + isValid, + parsed: isValid ? value : true, + messages: [message], + }; + }; + + const DEFAULT_MESSAGE = + "The evaluated value should either be an icon name, undefined, null, or an empty string. We currently use the icons from the Blueprint library. You can see the list of icons at https://blueprintjs.com/docs/#icons"; + + const isIconName = (value: unknown) => { + return ( + config?.params?.allowedValues?.includes(value as string) || + value === undefined || + value === null || + value === "" + ); + }; + + if (isIconName(value)) { + return generateResponseAndReturn(true); + } + + if (Array.isArray(value)) { + const isValid = value.every((item) => { + if (isIconName(item)) { + return true; + } + + if (Array.isArray(item)) { + return item.every((subItem) => isIconName(subItem)); + } + + return false; + }); + + return isValid + ? generateResponseAndReturn(true) + : generateResponseAndReturn(false, DEFAULT_MESSAGE); + } + + return generateResponseAndReturn(false, DEFAULT_MESSAGE); +} + +export function iconPositionForEachRowValidation( + value: unknown, + props: MenuButtonWidgetProps, + _: any, + moment: any, + propertyPath: string, + config: ValidationConfig, +): ValidationResponse { + const generateResponseAndReturn = (isValid = false, message = "") => { + return { + isValid, + parsed: isValid ? value : true, + messages: [message], + }; + }; + + const DEFAULT_MESSAGE = `The evaluated value should be one of the allowed values => ${config?.params?.allowedValues?.join( + ", ", + )}, undefined, null, or an empty string`; + + const isIconPosition = (value: unknown) => { + return ( + config?.params?.allowedValues?.includes(value as string) || + value === undefined || + value === null || + value === "" + ); + }; + + if (isIconPosition(value)) { + return generateResponseAndReturn(true); + } + + if (Array.isArray(value)) { + const isValid = value.every((item) => { + if (isIconPosition(item)) { + return true; + } + + if (Array.isArray(item)) { + return item.every((subItem) => isIconPosition(subItem)); + } + + return false; + }); + + return isValid + ? generateResponseAndReturn(true) + : generateResponseAndReturn(false, DEFAULT_MESSAGE); + } + + return generateResponseAndReturn(false, DEFAULT_MESSAGE); +} + +export function colorForEachRowValidation( + value: unknown, + props: MenuButtonWidgetProps, + _: any, + moment: any, + propertyPath: string, + config: ValidationConfig, +): ValidationResponse { + const generateResponseAndReturn = (isValid = false, message = "") => { + return { + isValid, + parsed: isValid ? value : true, + messages: [message], + }; + }; + + const DEFAULT_MESSAGE = `The evaluated value should match ${config?.params?.regex}`; + + const isColor = (value: unknown) => { + return config?.params?.regex?.test(value as string); + }; + + if (isColor(value)) { + return generateResponseAndReturn(true); + } + + if (Array.isArray(value)) { + const isValid = value.every((item) => { + if (isColor(item)) { + return true; + } + + if (Array.isArray(item)) { + return item.every((subItem) => isColor(subItem)); + } + + return false; + }); + + return isValid + ? generateResponseAndReturn(true) + : generateResponseAndReturn(false, DEFAULT_MESSAGE); + } + + return generateResponseAndReturn(false, DEFAULT_MESSAGE); +} diff --git a/app/client/src/widgets/MenuButtonWidget/widget/helper.test.ts b/app/client/src/widgets/MenuButtonWidget/widget/helper.test.ts index 19d21c59a8..6d4e7352d4 100644 --- a/app/client/src/widgets/MenuButtonWidget/widget/helper.test.ts +++ b/app/client/src/widgets/MenuButtonWidget/widget/helper.test.ts @@ -1,13 +1,29 @@ -import { getSourceDataKeysForEventAutocomplete } from "./helper"; +import { getKeysFromSourceDataForEventAutocomplete } from "./helper"; -describe("getSourceDataKeysForEventAutocomplete", () => { - it("Should test with valid values", () => { - const mockProps = { - sourceDataKeys: ["step", "task", "status", "action"], - menuItemsSource: "DYANMIC", - }; +describe("getKeysFromSourceDataForEventAutocomplete", () => { + it("Should test with valid values - array of objects", () => { + const mockProps = [ + { + step: "#1", + task: "Drop a table", + status: "✅", + action: "", + }, + { + step: "#2", + task: "Create a query fetch_users with the Mock DB", + status: "--", + action: "", + }, + { + step: "#3", + task: "Bind the query using => fetch_users.data", + status: "--", + action: "", + }, + ]; - const result = getSourceDataKeysForEventAutocomplete(mockProps as any); + const result = getKeysFromSourceDataForEventAutocomplete(mockProps as any); const expected = { currentItem: { step: "", @@ -19,25 +35,169 @@ describe("getSourceDataKeysForEventAutocomplete", () => { expect(result).toStrictEqual(expected); }); - it("Should test with Static menuItemSource", () => { - const mockProps = { - sourceDataKeys: [], - menuItemsSource: "STATIC", - }; + it("Should test with valid values - array of arrays of objects", () => { + const mockProps = [ + [ + { + gender: "male", + name: "#1 Victor", + email: "victor.garrett@example.com", + phone: "011-800-3906", + id: "6125683T", + nat: "IE", + }, + { + gender: "male", + name: "#1 Tobias", + email: "tobias.hansen@example.com", + phone: "84467012", + id: "200247-8744", + nat: "DK", + }, + { + gender: "female", + name: "#1 Jane", + email: "jane.coleman@example.com", + phone: "(679) 516-8766", + id: "098-73-7712", + nat: "US", + }, + { + gender: "female", + name: "#1 Yaromira", + email: "yaromira.manuylenko@example.com", + phone: "(099) B82-8594", + id: null, + nat: "UA", + }, + { + gender: "male", + name: "#1 Andre", + email: "andre.ortiz@example.com", + phone: "08-3115-5776", + id: "876838842", + nat: "AU", + }, + ], + [ + { + gender: "male", + name: "#2 Victor", + email: "victor.garrett@example.com", + phone: "011-800-3906", + id: "6125683T", + nat: "IE", + }, + { + gender: "male", + name: "#2 Tobias", + email: "tobias.hansen@example.com", + phone: "84467012", + id: "200247-8744", + nat: "DK", + }, + { + gender: "female", + name: "#2 Jane", + email: "jane.coleman@example.com", + phone: "(679) 516-8766", + id: "098-73-7712", + nat: "US", + }, + { + gender: "female", + name: "#2 Yaromira", + email: "yaromira.manuylenko@example.com", + phone: "(099) B82-8594", + id: null, + nat: "UA", + }, + { + gender: "male", + name: "#2 Andre", + email: "andre.ortiz@example.com", + phone: "08-3115-5776", + id: "876838842", + nat: "AU", + }, + ], + [ + { + gender: "male", + name: "#3 Victor", + email: "victor.garrett@example.com", + phone: "011-800-3906", + id: "6125683T", + nat: "IE", + }, + { + gender: "male", + name: "#3 Tobias", + email: "tobias.hansen@example.com", + phone: "84467012", + id: "200247-8744", + nat: "DK", + }, + { + gender: "female", + name: "#3 Jane", + email: "jane.coleman@example.com", + phone: "(679) 516-8766", + id: "098-73-7712", + nat: "US", + }, + { + gender: "female", + name: "#3 Yaromira", + email: "yaromira.manuylenko@example.com", + phone: "(099) B82-8594", + id: null, + nat: "UA", + }, + { + gender: "male", + name: "#3 Andre", + email: "andre.ortiz@example.com", + phone: "08-3115-5776", + id: "876838842", + nat: "AU", + }, + ], + ]; - const result = getSourceDataKeysForEventAutocomplete(mockProps as any); - const expected = undefined; + const result = getKeysFromSourceDataForEventAutocomplete(mockProps as any); + const expected = { + currentItem: { + gender: "", + name: "", + email: "", + phone: "", + id: "", + nat: "", + }, + }; expect(result).toStrictEqual(expected); }); - it("Should test with empty sourceDataKeys", () => { + it("Should test with empty sourceData", () => { const mockProps = { - sourceDataKeys: [], - menuItemsSource: "DYANMIC", + __evaluation__: { + evaluatedValues: { + sourceData: [], + }, + }, }; - const result = getSourceDataKeysForEventAutocomplete(mockProps as any); - const expected = undefined; + const result = getKeysFromSourceDataForEventAutocomplete(mockProps as any); + const expected = { currentItem: {} }; + expect(result).toStrictEqual(expected); + }); + + it("Should test without sourceData", () => { + const mockProps = {}; + + const result = getKeysFromSourceDataForEventAutocomplete(mockProps as any); + const expected = { currentItem: {} }; expect(result).toStrictEqual(expected); }); }); diff --git a/app/client/src/widgets/MenuButtonWidget/widget/helper.ts b/app/client/src/widgets/MenuButtonWidget/widget/helper.ts index 92945730a3..5c8cb8f84a 100644 --- a/app/client/src/widgets/MenuButtonWidget/widget/helper.ts +++ b/app/client/src/widgets/MenuButtonWidget/widget/helper.ts @@ -1,34 +1,41 @@ import { isArray } from "lodash"; -import { MenuButtonWidgetProps, MenuItemsSource } from "../constants"; -export const getSourceDataKeysForEventAutocomplete = ( - props: MenuButtonWidgetProps, +export const getKeysFromSourceDataForEventAutocomplete = ( + sourceData?: Array> | unknown, ) => { - if ( - props.menuItemsSource === MenuItemsSource.STATIC || - !props.sourceDataKeys?.length - ) { - return; - } + if (isArray(sourceData) && sourceData?.length) { + const keys = getUniqueKeysFromSourceData(sourceData); - return { - currentItem: props.sourceDataKeys.reduce( - (prev, cur) => ({ ...prev, [cur]: "" }), - {}, - ), - }; + return { + currentItem: keys.reduce((prev, cur) => ({ ...prev, [cur]: "" }), {}), + }; + } else { + return { currentItem: {} }; + } }; -export const getSourceDataKeys = (props: MenuButtonWidgetProps) => { - if (!isArray(props.sourceData) || !props.sourceData?.length) { +export const getUniqueKeysFromSourceData = ( + sourceData?: Array>, +) => { + if (!isArray(sourceData) || !sourceData?.length) { return []; } const allKeys: string[] = []; // get all keys - props.sourceData?.forEach((item) => allKeys.push(...Object.keys(item))); + sourceData?.forEach((item) => { + if (isArray(item) && item?.length) { + item.forEach((subItem) => { + allKeys.push(...Object.keys(subItem)); + }); + } else { + allKeys.push(...Object.keys(item)); + } + }); // return unique keys - return [...new Set(allKeys)]; + const uniqueKeys = [...new Set(allKeys)]; + + return uniqueKeys; }; diff --git a/app/client/src/widgets/MenuButtonWidget/widget/index.tsx b/app/client/src/widgets/MenuButtonWidget/widget/index.tsx index 10816a2446..ae8e56cc2e 100644 --- a/app/client/src/widgets/MenuButtonWidget/widget/index.tsx +++ b/app/client/src/widgets/MenuButtonWidget/widget/index.tsx @@ -9,9 +9,7 @@ import { MinimumPopupRows } from "widgets/constants"; import { MenuButtonWidgetProps, MenuItem, MenuItemsSource } from "../constants"; import contentConfig from "./propertyConfig/contentConfig"; import styleConfig from "./propertyConfig/styleConfig"; -import equal from "fast-deep-equal/es6"; import { isArray, orderBy } from "lodash"; -import { getSourceDataKeys } from "./helper"; import { Stylesheet } from "entities/AppTheming"; class MenuButtonWidget extends BaseWidget { @@ -108,19 +106,6 @@ class MenuButtonWidget extends BaseWidget { return []; }; - componentDidMount = () => { - super.updateWidgetProperty("sourceDataKeys", getSourceDataKeys(this.props)); - }; - - componentDidUpdate = (prevProps: MenuButtonWidgetProps) => { - if (!equal(prevProps.sourceData, this.props.sourceData)) { - super.updateWidgetProperty( - "sourceDataKeys", - getSourceDataKeys(this.props), - ); - } - }; - getPageView() { const { componentWidth } = this.getComponentDimensions(); const menuDropDownWidth = MinimumPopupRows * this.props.parentColumnSpace; diff --git a/app/client/src/widgets/MenuButtonWidget/widget/propertyConfig/childPanels/configureMenuItemsConfig.ts b/app/client/src/widgets/MenuButtonWidget/widget/propertyConfig/childPanels/configureMenuItemsConfig.ts index 1992382ca6..3e27c77340 100644 --- a/app/client/src/widgets/MenuButtonWidget/widget/propertyConfig/childPanels/configureMenuItemsConfig.ts +++ b/app/client/src/widgets/MenuButtonWidget/widget/propertyConfig/childPanels/configureMenuItemsConfig.ts @@ -1,19 +1,11 @@ import { ValidationTypes } from "constants/WidgetValidation"; -import { ICON_NAMES } from "../../../constants"; -import { getSourceDataKeysForEventAutocomplete } from "../../helper"; +import { ICON_NAMES, MenuButtonWidgetProps } from "../../../constants"; +import { getKeysFromSourceDataForEventAutocomplete } from "../../helper"; export default { editableTitle: false, titlePropertyName: "label", panelIdPropertyName: "id", - updateHook: (props: any, propertyPath: string, propertyValue: string) => { - return [ - { - propertyPath, - propertyValue, - }, - ]; - }, contentChildren: [ { sectionName: "General", @@ -33,7 +25,7 @@ export default { type: ValidationTypes.TEXT, }, }, - dependencies: ["sourceDataKeys"], + evaluatedDependencies: ["sourceData"], }, { propertyName: "isVisible", @@ -51,7 +43,7 @@ export default { }, }, customJSControl: "MENU_BUTTON_DYNAMIC_ITEMS", - dependencies: ["sourceDataKeys"], + evaluatedDependencies: ["sourceData"], }, { propertyName: "isDisabled", @@ -69,7 +61,7 @@ export default { }, }, customJSControl: "MENU_BUTTON_DYNAMIC_ITEMS", - dependencies: ["sourceDataKeys"], + evaluatedDependencies: ["sourceData"], }, ], }, @@ -85,8 +77,12 @@ export default { isJSConvertible: true, isBindProperty: true, isTriggerProperty: true, - additionalAutoComplete: getSourceDataKeysForEventAutocomplete, - dependencies: ["sourceDataKeys"], + additionalAutoComplete: (props: MenuButtonWidgetProps) => { + return getKeysFromSourceDataForEventAutocomplete( + props?.__evaluation__?.evaluatedValues?.sourceData, + ); + }, + evaluatedDependencies: ["sourceData"], }, ], }, @@ -114,7 +110,7 @@ export default { }, }, customJSControl: "MENU_BUTTON_DYNAMIC_ITEMS", - dependencies: ["sourceDataKeys"], + evaluatedDependencies: ["sourceData"], }, { propertyName: "iconAlign", @@ -145,7 +141,7 @@ export default { }, }, customJSControl: "MENU_BUTTON_DYNAMIC_ITEMS", - dependencies: ["sourceDataKeys"], + evaluatedDependencies: ["sourceData"], }, ], }, @@ -162,7 +158,7 @@ export default { isTriggerProperty: false, isJSConvertible: true, customJSControl: "MENU_BUTTON_DYNAMIC_ITEMS", - dependencies: ["sourceDataKeys"], + evaluatedDependencies: ["sourceData"], validation: { type: ValidationTypes.ARRAY_OF_TYPE_OR_TYPE, params: { @@ -181,7 +177,7 @@ export default { isTriggerProperty: false, isJSConvertible: true, customJSControl: "MENU_BUTTON_DYNAMIC_ITEMS", - dependencies: ["sourceDataKeys"], + evaluatedDependencies: ["sourceData"], validation: { type: ValidationTypes.ARRAY_OF_TYPE_OR_TYPE, params: { @@ -200,7 +196,7 @@ export default { isTriggerProperty: false, isJSConvertible: true, customJSControl: "MENU_BUTTON_DYNAMIC_ITEMS", - dependencies: ["sourceDataKeys"], + evaluatedDependencies: ["sourceData"], validation: { type: ValidationTypes.ARRAY_OF_TYPE_OR_TYPE, params: { diff --git a/app/client/src/widgets/MenuButtonWidget/widget/propertyConfig/propertyUtils.ts b/app/client/src/widgets/MenuButtonWidget/widget/propertyConfig/propertyUtils.ts index e87a23c157..926c43b740 100644 --- a/app/client/src/widgets/MenuButtonWidget/widget/propertyConfig/propertyUtils.ts +++ b/app/client/src/widgets/MenuButtonWidget/widget/propertyConfig/propertyUtils.ts @@ -19,10 +19,6 @@ export const updateMenuItemsSource = ( propertyPath: "sourceData", propertyValue: [], }); - propertiesToUpdate.push({ - propertyPath: "sourceDataKeys", - propertyValue: [], - }); } if (!props.configureMenuItems) { diff --git a/app/client/src/widgets/TableWidgetV2/component/Constants.ts b/app/client/src/widgets/TableWidgetV2/component/Constants.ts index fdeb52b71a..1d4f1e3603 100644 --- a/app/client/src/widgets/TableWidgetV2/component/Constants.ts +++ b/app/client/src/widgets/TableWidgetV2/component/Constants.ts @@ -8,6 +8,12 @@ import { ButtonVariant, } from "components/constants"; import { DropdownOption } from "widgets/SelectWidget/constants"; +import { + ConfigureMenuItems, + MenuItem, + MenuItems, + MenuItemsSource, +} from "widgets/MenuButtonWidget/constants"; import { ColumnTypes } from "../constants"; export type TableSizes = { @@ -155,6 +161,9 @@ export interface MenuButtonCellProperties { menuColor?: string; menuButtoniconName?: IconName; onItemClicked?: (onClick: string | undefined) => void; + menuItemsSource: MenuItemsSource; + configureMenuItems: ConfigureMenuItems; + sourceData?: Array>; } export interface URLCellProperties { @@ -199,24 +208,6 @@ export interface CellLayoutProperties ImageCellProperties, BaseCellProperties {} -export type MenuItems = Record< - string, - { - widgetId: string; - id: string; - index: number; - isVisible?: boolean; - isDisabled?: boolean; - label?: string; - backgroundColor?: string; - textColor?: string; - iconName?: IconName; - iconColor?: string; - iconAlign?: Alignment; - onClick?: string; - } ->; - export interface TableColumnMetaProps { isHidden: boolean; format?: string; @@ -339,6 +330,10 @@ export interface ColumnProperties onItemClicked?: (onClick: string | undefined) => void; iconButtonStyle?: ButtonStyleType; imageSize?: ImageSize; + getVisibleItems?: () => Array; + menuItemsSource?: MenuItemsSource; + configureMenuItems?: ConfigureMenuItems; + sourceData?: Array>; } export const ConditionFunctions: { diff --git a/app/client/src/widgets/TableWidgetV2/component/cellComponents/MenuButtonCell.tsx b/app/client/src/widgets/TableWidgetV2/component/cellComponents/MenuButtonCell.tsx index b67742e918..19e684dd1d 100644 --- a/app/client/src/widgets/TableWidgetV2/component/cellComponents/MenuButtonCell.tsx +++ b/app/client/src/widgets/TableWidgetV2/component/cellComponents/MenuButtonCell.tsx @@ -2,11 +2,17 @@ import React from "react"; import { IconName } from "@blueprintjs/icons"; import { Alignment } from "@blueprintjs/core"; -import { BaseCellComponentProps, MenuItems } from "../Constants"; +import { BaseCellComponentProps } from "../Constants"; import { ButtonVariant } from "components/constants"; import { CellWrapper } from "../TableStyledWrappers"; import { ColumnAction } from "components/propertyControls/ColumnActionSelectorControl"; import MenuButtonTableComponent from "./menuButtonTableComponent"; +import { + ConfigureMenuItems, + MenuItem, + MenuItems, + MenuItemsSource, +} from "widgets/MenuButtonWidget/constants"; interface MenuButtonProps extends Omit { action?: ColumnAction; @@ -16,6 +22,8 @@ function MenuButton({ borderRadius, boxShadow, compactMode, + configureMenuItems, + getVisibleItems, iconAlign, iconName, isCompact, @@ -24,9 +32,11 @@ function MenuButton({ label, menuColor, menuItems, + menuItemsSource, menuVariant, onCommandClick, rowIndex, + sourceData, }: MenuButtonProps): JSX.Element { const handlePropagation = ( e: React.MouseEvent, @@ -35,9 +45,9 @@ function MenuButton({ e.stopPropagation(); } }; - const onItemClicked = (onClick?: string) => { + const onItemClicked = (onClick?: string, index?: number) => { if (onClick) { - onCommandClick(onClick); + onCommandClick(onClick, index); } }; @@ -47,6 +57,8 @@ function MenuButton({ borderRadius={borderRadius} boxShadow={boxShadow} compactMode={compactMode} + configureMenuItems={configureMenuItems} + getVisibleItems={getVisibleItems} iconAlign={iconAlign} iconName={iconName} isCompact={isCompact} @@ -54,9 +66,11 @@ function MenuButton({ label={label} menuColor={menuColor} menuItems={{ ...menuItems }} + menuItemsSource={menuItemsSource} menuVariant={menuVariant} onItemClicked={onItemClicked} rowIndex={rowIndex} + sourceData={sourceData} /> ); @@ -66,7 +80,11 @@ export interface RenderMenuButtonProps extends BaseCellComponentProps { isSelected: boolean; label: string; isDisabled: boolean; - onCommandClick: (dynamicTrigger: string, onComplete?: () => void) => void; + onCommandClick: ( + dynamicTrigger: string, + index?: number, + onComplete?: () => void, + ) => void; isCompact?: boolean; menuItems: MenuItems; menuVariant?: ButtonVariant; @@ -76,6 +94,10 @@ export interface RenderMenuButtonProps extends BaseCellComponentProps { iconName?: IconName; iconAlign?: Alignment; rowIndex: number; + getVisibleItems: (rowIndex: number) => Array; + menuItemsSource: MenuItemsSource; + configureMenuItems: ConfigureMenuItems; + sourceData?: Array>; } export function MenuButtonCell(props: RenderMenuButtonProps) { diff --git a/app/client/src/widgets/TableWidgetV2/component/cellComponents/menuButtonTableComponent.tsx b/app/client/src/widgets/TableWidgetV2/component/cellComponents/menuButtonTableComponent.tsx index 9cfe818778..ba145835d8 100644 --- a/app/client/src/widgets/TableWidgetV2/component/cellComponents/menuButtonTableComponent.tsx +++ b/app/client/src/widgets/TableWidgetV2/component/cellComponents/menuButtonTableComponent.tsx @@ -3,11 +3,11 @@ import styled, { createGlobalStyle } from "styled-components"; import { Alignment, Button, - Classes as CoreClasses, + Classes as BlueprintCoreClasses, Icon, Menu, - MenuItem, - Classes as BClasses, + MenuItem as BlueprintMenuItem, + Classes as BlueprintClasses, } from "@blueprintjs/core"; import { Classes, Popover2 } from "@blueprintjs/popover2"; import { IconName } from "@blueprintjs/icons"; @@ -20,15 +20,19 @@ import { } from "widgets/WidgetUtils"; import { darkenActive, darkenHover } from "constants/DefaultTheme"; import { ButtonVariant, ButtonVariantTypes } from "components/constants"; -import { MenuItems } from "../Constants"; import tinycolor from "tinycolor2"; import { Colors } from "constants/Colors"; -import orderBy from "lodash/orderBy"; import { getBooleanPropertyValue, getPropertyValue, } from "widgets/TableWidgetV2/widget/utilities"; import { ThemeProp } from "widgets/constants"; +import { + ConfigureMenuItems, + MenuItem, + MenuItems, + MenuItemsSource, +} from "widgets/MenuButtonWidget/constants"; const MenuButtonContainer = styled.div` width: 100%; @@ -54,7 +58,7 @@ const PopoverStyles = createGlobalStyle<{ borderRadius >= `1.5rem` ? `0.375rem` : borderRadius}; overflow: hidden; } - & .${BClasses.MENU_ITEM} { + & .${BlueprintClasses.MENU_ITEM} { padding: 9px 12px; border-radius: 0; &:hover { @@ -153,8 +157,8 @@ const BaseButton = styled(Button)` box-shadow: ${({ boxShadow }) => `${boxShadow}`} !important; `; -const BaseMenuItem = styled(MenuItem)` - &.${CoreClasses.MENU_ITEM}.${CoreClasses.DISABLED} { +const BaseMenuItem = styled(BlueprintMenuItem)` + &.${BlueprintCoreClasses.MENU_ITEM}.${BlueprintCoreClasses.DISABLED} { background-color: ${Colors.GREY_1} !important; } ${({ backgroundColor, theme }) => @@ -206,64 +210,71 @@ const StyledMenu = styled(Menu)` interface PopoverContentProps { menuItems: MenuItems; - onItemClicked: (onClick: string | undefined) => void; + onItemClicked: ( + onClick: string | undefined, + index?: number, + onComplete?: () => void, + ) => void; + getVisibleItems: (rowIndex: number) => Array; isCompact?: boolean; rowIndex: number; + menuItemsSource: MenuItemsSource; + configureMenuItems: ConfigureMenuItems; + sourceData?: Array>; } function PopoverContent(props: PopoverContentProps) { - const { isCompact, menuItems: itemsObj, onItemClicked, rowIndex } = props; + const { getVisibleItems, isCompact, onItemClicked, rowIndex } = props; - if (!itemsObj) return ; - const visibleItems = Object.keys(itemsObj) - .map((itemKey) => itemsObj[itemKey]) - .filter((item) => getBooleanPropertyValue(item.isVisible, rowIndex)); + const visibleItems = getVisibleItems(rowIndex); - const items = orderBy(visibleItems, ["index"], ["asc"]); + if (!visibleItems?.length) { + return ; + } else { + const listItems = visibleItems.map((item: MenuItem, index: number) => { + const { + backgroundColor, + iconAlign, + iconColor, + iconName, + id, + isDisabled, + label, + onClick, + textColor, + } = item; - const listItems = items.map((menuItem) => { - const { - backgroundColor, - iconAlign, - iconColor, - iconName, - id, - isDisabled, - label, - onClick, - textColor, - } = menuItem; + return ( + + ) : ( + undefined + ) + } + isCompact={isCompact} + key={id} + labelElement={ + iconAlign === Alignment.RIGHT && iconName ? ( + + ) : ( + undefined + ) + } + onClick={() => onItemClicked(onClick, index)} + text={label} + textColor={getPropertyValue(textColor, rowIndex)} + /> + ); + }); - return ( - - ) : ( - undefined - ) - } - isCompact={isCompact} - key={id} - labelElement={ - iconAlign === Alignment.RIGHT ? ( - - ) : ( - undefined - ) - } - onClick={() => onItemClicked(onClick)} - text={label} - textColor={getPropertyValue(textColor, rowIndex)} - /> - ); - }); - - return {listItems}; + return {listItems}; + } } interface PopoverTargetButtonProps { @@ -315,6 +326,7 @@ export interface MenuButtonComponentProps { isVisible?: boolean; isCompact?: boolean; menuItems: MenuItems; + getVisibleItems: (rowIndex: number) => Array; menuVariant?: ButtonVariant; menuColor?: string; borderRadius?: string; @@ -322,9 +334,16 @@ export interface MenuButtonComponentProps { boxShadowColor?: string; iconName?: IconName; iconAlign?: Alignment; - onItemClicked: (onClick: string | undefined) => void; + onItemClicked: ( + onClick: string | undefined, + index?: number, + onComplete?: () => void, + ) => void; rowIndex: number; compactMode?: string; + menuItemsSource: MenuItemsSource; + configureMenuItems: ConfigureMenuItems; + sourceData?: Array>; } function MenuButtonTableComponent(props: MenuButtonComponentProps) { @@ -333,6 +352,8 @@ function MenuButtonTableComponent(props: MenuButtonComponentProps) { boxShadow, boxShadowColor, compactMode, + configureMenuItems, + getVisibleItems, iconAlign, iconName, isCompact, @@ -340,9 +361,11 @@ function MenuButtonTableComponent(props: MenuButtonComponentProps) { label, menuColor = "#e1e1e1", menuItems, + menuItemsSource, menuVariant, onItemClicked, rowIndex, + sourceData, } = props; return ( @@ -359,10 +382,14 @@ function MenuButtonTableComponent(props: MenuButtonComponentProps) { }} content={ } disabled={isDisabled} diff --git a/app/client/src/widgets/TableWidgetV2/widget/index.tsx b/app/client/src/widgets/TableWidgetV2/widget/index.tsx index 942e1811f7..028c048c8b 100644 --- a/app/client/src/widgets/TableWidgetV2/widget/index.tsx +++ b/app/client/src/widgets/TableWidgetV2/widget/index.tsx @@ -13,6 +13,7 @@ import _, { isEmpty, union, isObject, + orderBy, } from "lodash"; import BaseWidget, { WidgetState } from "widgets/BaseWidget"; @@ -58,6 +59,7 @@ import { getCellProperties, isColumnTypeEditable, getColumnType, + getBooleanPropertyValue, } from "./utilities"; import { ColumnProperties, @@ -85,6 +87,7 @@ import { SwitchCell } from "../component/cellComponents/SwitchCell"; import { SelectCell } from "../component/cellComponents/SelectCell"; import { CellWrapper } from "../component/TableStyledWrappers"; import { Stylesheet } from "entities/AppTheming"; +import { MenuItem, MenuItemsSource } from "widgets/MenuButtonWidget/constants"; const ReactTableComponent = lazy(() => retryPromise(() => import("../component")), @@ -1585,6 +1588,70 @@ class TableWidgetV2 extends BaseWidget { ); case ColumnTypes.MENU_BUTTON: + const getVisibleItems = (rowIndex: number) => { + const { + configureMenuItems, + menuItems, + menuItemsSource, + sourceData, + } = cellProperties; + + if (menuItemsSource === MenuItemsSource.STATIC && menuItems) { + const visibleItems = Object.values(menuItems)?.filter((item) => + getBooleanPropertyValue(item.isVisible, rowIndex), + ); + + return visibleItems?.length + ? orderBy(visibleItems, ["index"], ["asc"]) + : []; + } else if ( + menuItemsSource === MenuItemsSource.DYNAMIC && + isArray(sourceData) && + sourceData?.length && + configureMenuItems?.config + ) { + const { config } = configureMenuItems; + + const getValue = ( + propertyName: keyof MenuItem, + index: number, + rowIndex: number, + ) => { + const value = config[propertyName]; + + if (isArray(value) && isArray(value[rowIndex])) { + return value[rowIndex][index]; + } else if (isArray(value)) { + return value[index]; + } + + return value ?? null; + }; + + const visibleItems = sourceData + .map((item, index) => ({ + ...item, + id: index.toString(), + isVisible: getValue("isVisible", index, rowIndex), + isDisabled: getValue("isDisabled", index, rowIndex), + index: index, + widgetId: "", + label: getValue("label", index, rowIndex), + onClick: config?.onClick, + textColor: getValue("textColor", index, rowIndex), + backgroundColor: getValue("backgroundColor", index, rowIndex), + iconAlign: getValue("iconAlign", index, rowIndex), + iconColor: getValue("iconColor", index, rowIndex), + iconName: getValue("iconName", index, rowIndex), + })) + .filter((item) => item.isVisible === true); + + return visibleItems; + } + + return []; + }; + return ( { boxShadow={cellProperties.boxShadow} cellBackground={cellProperties.cellBackground} compactMode={compactMode} + configureMenuItems={cellProperties.configureMenuItems} fontStyle={cellProperties.fontStyle} + getVisibleItems={getVisibleItems} horizontalAlignment={cellProperties.horizontalAlignment} iconAlign={cellProperties.iconAlign} iconName={cellProperties.menuButtoniconName || undefined} @@ -1609,17 +1678,34 @@ class TableWidgetV2 extends BaseWidget { cellProperties.menuColor || this.props.accentColor || Colors.GREEN } menuItems={cellProperties.menuItems} + menuItemsSource={cellProperties.menuItemsSource} menuVariant={cellProperties.menuVariant ?? DEFAULT_MENU_VARIANT} - onCommandClick={(action: string, onComplete?: () => void) => - this.onColumnEvent({ + onCommandClick={( + action: string, + index?: number, + onComplete?: () => void, + ) => { + const additionalData: Record< + string, + string | number | Record + > = {}; + + if (cellProperties?.sourceData && _.isNumber(index)) { + additionalData.currentItem = cellProperties.sourceData[index]; + additionalData.currentIndex = index; + } + + return this.onColumnEvent({ rowIndex, action, onComplete, triggerPropertyName: "onClick", eventType: EventType.ON_CLICK, - }) - } + additionalData, + }); + }} rowIndex={originalIndex} + sourceData={cellProperties.sourceData} textColor={cellProperties.textColor} textSize={cellProperties.textSize} verticalAlignment={cellProperties.verticalAlignment} diff --git a/app/client/src/widgets/TableWidgetV2/widget/propertyConfig/PanelConfig/Basic.ts b/app/client/src/widgets/TableWidgetV2/widget/propertyConfig/PanelConfig/Basic.ts index 8b3c850fc5..4a9c5e59f5 100644 --- a/app/client/src/widgets/TableWidgetV2/widget/propertyConfig/PanelConfig/Basic.ts +++ b/app/client/src/widgets/TableWidgetV2/widget/propertyConfig/PanelConfig/Basic.ts @@ -4,8 +4,19 @@ import { ICON_NAMES, TableWidgetProps, } from "widgets/TableWidgetV2/constants"; -import { hideByColumnType, updateIconAlignment } from "../../propertyUtils"; +import { + hideByColumnType, + hideByMenuItemsSource, + hideIfMenuItemsSourceDataIsFalsy, + updateIconAlignment, + updateMenuItemsSource, +} from "../../propertyUtils"; import { IconNames } from "@blueprintjs/icons"; +import { MenuItemsSource } from "widgets/MenuButtonWidget/constants"; +import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory"; +import { AutocompleteDataType } from "utils/autocomplete/CodemirrorTernService"; +import { sourceDataArrayValidation } from "widgets/MenuButtonWidget/validations"; +import configureMenuItemsConfig from "./childPanels/configureMenuItemsConfig"; export default { sectionName: "Basic", @@ -70,6 +81,104 @@ export default { isBindProperty: true, isTriggerProperty: false, }, + { + propertyName: "menuItemsSource", + helpText: "Sets the source for the menu items", + label: "Menu Items Source", + controlType: "ICON_TABS", + fullWidth: true, + defaultValue: MenuItemsSource.STATIC, + options: [ + { + label: "Static", + value: MenuItemsSource.STATIC, + }, + { + label: "Dynamic", + value: MenuItemsSource.DYNAMIC, + }, + ], + isJSConvertible: false, + isBindProperty: false, + isTriggerProperty: false, + validation: { type: ValidationTypes.TEXT }, + updateHook: updateMenuItemsSource, + dependencies: [ + "primaryColumns", + "columnOrder", + "sourceData", + "configureMenuItems", + ], + hidden: (props: TableWidgetProps, propertyPath: string) => { + return hideByColumnType( + props, + propertyPath, + [ColumnTypes.MENU_BUTTON], + false, + ); + }, + }, + { + helpText: "Takes in an array of items to display the menu items.", + propertyName: "sourceData", + label: "Source Data", + controlType: "TABLE_COMPUTE_VALUE", + placeholderText: "{{Query1.data}}", + isBindProperty: true, + isTriggerProperty: false, + validation: { + type: ValidationTypes.FUNCTION, + params: { + expected: { + type: "Array of values", + example: `['option1', 'option2'] | [{ "label": "label1", "value": "value1" }]`, + autocompleteDataType: AutocompleteDataType.ARRAY, + }, + fnString: sourceDataArrayValidation.toString(), + }, + }, + evaluationSubstitutionType: EvaluationSubstitutionType.SMART_SUBSTITUTE, + hidden: (props: TableWidgetProps, propertyPath: string) => { + return ( + hideByColumnType( + props, + propertyPath, + [ColumnTypes.MENU_BUTTON], + false, + ) || + hideByMenuItemsSource(props, propertyPath, MenuItemsSource.STATIC) + ); + }, + dependencies: ["primaryColumns", "columnOrder", "menuItemsSource"], + }, + { + helpText: "Configure how each menu item will appear.", + propertyName: "configureMenuItems", + controlType: "OPEN_CONFIG_PANEL", + buttonConfig: { + label: "Item Configuration", + icon: "settings-2-line", + }, + label: "Configure Menu Items", + isBindProperty: false, + isTriggerProperty: false, + hidden: (props: TableWidgetProps, propertyPath: string) => + hideByColumnType( + props, + propertyPath, + [ColumnTypes.MENU_BUTTON], + false, + ) || + hideIfMenuItemsSourceDataIsFalsy(props, propertyPath) || + hideByMenuItemsSource(props, propertyPath, MenuItemsSource.STATIC), + dependencies: [ + "primaryColumns", + "columnOrder", + "menuItemsSource", + "sourceData", + ], + panelConfig: configureMenuItemsConfig, + }, { helpText: "Menu items", propertyName: "menuItems", @@ -78,11 +187,14 @@ export default { isBindProperty: false, isTriggerProperty: false, hidden: (props: TableWidgetProps, propertyPath: string) => { - return hideByColumnType( - props, - propertyPath, - [ColumnTypes.MENU_BUTTON], - false, + return ( + hideByColumnType( + props, + propertyPath, + [ColumnTypes.MENU_BUTTON], + false, + ) || + hideByMenuItemsSource(props, propertyPath, MenuItemsSource.DYNAMIC) ); }, dependencies: ["primaryColumns", "columnOrder"], diff --git a/app/client/src/widgets/TableWidgetV2/widget/propertyConfig/PanelConfig/Data.ts b/app/client/src/widgets/TableWidgetV2/widget/propertyConfig/PanelConfig/Data.ts index cf2809e41b..4a3a912eae 100644 --- a/app/client/src/widgets/TableWidgetV2/widget/propertyConfig/PanelConfig/Data.ts +++ b/app/client/src/widgets/TableWidgetV2/widget/propertyConfig/PanelConfig/Data.ts @@ -10,6 +10,7 @@ import { hideByColumnType, showByColumnType, uniqueColumnAliasValidation, + updateMenuItemsSource, updateNumberColumnTypeTextAlignment, updateThemeStylesheetsInColumns, } from "../../propertyUtils"; @@ -78,6 +79,7 @@ export default { updateHook: composePropertyUpdateHook([ updateNumberColumnTypeTextAlignment, updateThemeStylesheetsInColumns, + updateMenuItemsSource, ]), dependencies: ["primaryColumns", "columnOrder", "childStylesheet"], isBindProperty: false, diff --git a/app/client/src/widgets/TableWidgetV2/widget/propertyConfig/PanelConfig/childPanels/configureMenuItemsConfig.ts b/app/client/src/widgets/TableWidgetV2/widget/propertyConfig/PanelConfig/childPanels/configureMenuItemsConfig.ts new file mode 100644 index 0000000000..3292e289e7 --- /dev/null +++ b/app/client/src/widgets/TableWidgetV2/widget/propertyConfig/PanelConfig/childPanels/configureMenuItemsConfig.ts @@ -0,0 +1,216 @@ +import { ValidationTypes } from "constants/WidgetValidation"; +import { AutocompleteDataType } from "utils/autocomplete/CodemirrorTernService"; +import { ICON_NAMES } from "widgets/MenuButtonWidget/constants"; +import { + booleanForEachRowValidation, + colorForEachRowValidation, + iconNamesForEachRowValidation, + iconPositionForEachRowValidation, + textForEachRowValidation, +} from "widgets/MenuButtonWidget/validations"; +import { getSourceDataAndCaluclateKeysForEventAutoComplete } from "widgets/TableWidgetV2/widget/utilities"; + +export default { + editableTitle: false, + titlePropertyName: "label", + panelIdPropertyName: "id", + contentChildren: [ + { + sectionName: "General", + children: [ + { + propertyName: "label", + helpText: + "Sets the label of a menu item using the {{currentItem}} binding.", + label: "Label", + controlType: "MENU_BUTTON_DYNAMIC_ITEMS", + placeholderText: "{{currentItem.name}}", + isBindProperty: true, + isTriggerProperty: false, + validation: { + type: ValidationTypes.FUNCTION, + params: { + expected: { + type: "Array of values", + example: `['option1', 'option2'] | [{ "label": "label1", "value": "value1" }]`, + autocompleteDataType: AutocompleteDataType.ARRAY, + }, + fnString: textForEachRowValidation.toString(), + }, + }, + evaluatedDependencies: ["primaryColumns"], + }, + { + propertyName: "isVisible", + helpText: + "Controls the visibility of the widget. Can also be configured the using {{currentItem}} binding.", + label: "Visible", + controlType: "SWITCH", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { + type: ValidationTypes.FUNCTION, + params: { + fnString: booleanForEachRowValidation.toString(), + }, + }, + customJSControl: "MENU_BUTTON_DYNAMIC_ITEMS", + evaluatedDependencies: ["primaryColumns"], + }, + { + propertyName: "isDisabled", + helpText: + "Disables input to the widget. Can also be configured the using {{currentItem}} binding.", + label: "Disabled", + controlType: "SWITCH", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { + type: ValidationTypes.FUNCTION, + params: { + fnString: booleanForEachRowValidation.toString(), + }, + }, + customJSControl: "MENU_BUTTON_DYNAMIC_ITEMS", + evaluatedDependencies: ["primaryColumns"], + }, + ], + }, + { + sectionName: "Events", + children: [ + { + helpText: + "Triggers an action when the menu item is clicked. Can also be configured the using {{currentItem}} binding.", + propertyName: "onClick", + label: "onClick", + controlType: "ACTION_SELECTOR", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: true, + additionalAutoComplete: getSourceDataAndCaluclateKeysForEventAutoComplete, + evaluatedDependencies: ["primaryColumns"], + }, + ], + }, + ], + styleChildren: [ + { + sectionName: "Icon", + children: [ + { + propertyName: "iconName", + label: "Icon", + helpText: + "Sets the icon to be used for a menu item. Can also be configured the using {{currentItem}} binding.", + controlType: "ICON_SELECT", + isBindProperty: true, + isTriggerProperty: false, + isJSConvertible: true, + validation: { + type: ValidationTypes.FUNCTION, + params: { + allowedValues: ICON_NAMES, + fnString: iconNamesForEachRowValidation.toString(), + }, + }, + customJSControl: "MENU_BUTTON_DYNAMIC_ITEMS", + evaluatedDependencies: ["primaryColumns"], + }, + { + propertyName: "iconAlign", + label: "Position", + helpText: + "Sets the icon alignment of a menu item. Can also be configured the using {{currentItem}} binding.", + controlType: "ICON_TABS", + options: [ + { + icon: "VERTICAL_LEFT", + value: "left", + }, + { + icon: "VERTICAL_RIGHT", + value: "right", + }, + ], + isBindProperty: true, + isTriggerProperty: false, + isJSConvertible: true, + validation: { + type: ValidationTypes.FUNCTION, + params: { + allowedValues: ["center", "left", "right"], + fnString: iconPositionForEachRowValidation.toString(), + }, + }, + customJSControl: "MENU_BUTTON_DYNAMIC_ITEMS", + evaluatedDependencies: ["primaryColumns"], + }, + ], + }, + { + sectionName: "Color", + children: [ + { + propertyName: "iconColor", + helpText: + "Sets the icon color of a menu item. Can also be configured the using {{currentItem}} binding.", + label: "Icon color", + controlType: "COLOR_PICKER", + isBindProperty: true, + isTriggerProperty: false, + isJSConvertible: true, + customJSControl: "MENU_BUTTON_DYNAMIC_ITEMS", + evaluatedDependencies: ["primaryColumns"], + validation: { + type: ValidationTypes.FUNCTION, + params: { + regex: /^(?![<|{{]).+/, + fnString: colorForEachRowValidation.toString(), + }, + }, + }, + { + propertyName: "backgroundColor", + helpText: + "Sets the background color of a menu item. Can also be configured the using {{currentItem}} binding.", + label: "Background color", + controlType: "COLOR_PICKER", + isBindProperty: true, + isTriggerProperty: false, + isJSConvertible: true, + customJSControl: "MENU_BUTTON_DYNAMIC_ITEMS", + evaluatedDependencies: ["primaryColumns"], + validation: { + type: ValidationTypes.FUNCTION, + params: { + regex: /^(?![<|{{]).+/, + fnString: colorForEachRowValidation.toString(), + }, + }, + }, + { + propertyName: "textColor", + helpText: + "Sets the text color of a menu item. Can also be configured the using {{currentItem}} binding.", + label: "Text color", + controlType: "COLOR_PICKER", + isBindProperty: true, + isTriggerProperty: false, + isJSConvertible: true, + customJSControl: "MENU_BUTTON_DYNAMIC_ITEMS", + evaluatedDependencies: ["primaryColumns"], + validation: { + type: ValidationTypes.FUNCTION, + params: { + regex: /^(?![<|{{]).+/, + fnString: colorForEachRowValidation.toString(), + }, + }, + }, + ], + }, + ], +}; diff --git a/app/client/src/widgets/TableWidgetV2/widget/propertyUtils.ts b/app/client/src/widgets/TableWidgetV2/widget/propertyUtils.ts index 56b594a7df..46c7985684 100644 --- a/app/client/src/widgets/TableWidgetV2/widget/propertyUtils.ts +++ b/app/client/src/widgets/TableWidgetV2/widget/propertyUtils.ts @@ -13,6 +13,7 @@ import { } from "utils/DynamicBindingUtils"; import { createEditActionColumn } from "./utilities"; import { PropertyHookUpdates } from "constants/PropertyControlConstants"; +import { MenuItemsSource } from "widgets/MenuButtonWidget/constants"; export function totalRecordsCountValidation( value: unknown, @@ -642,6 +643,86 @@ export const updateCustomColumnAliasOnLabelChange = ( } }; +export const hideByMenuItemsSource = ( + props: TableWidgetProps, + propertyPath: string, + menuItemsSource: MenuItemsSource, +) => { + const baseProperty = getBasePropertyPath(propertyPath); + const currentMenuItemsSource = get( + props, + `${baseProperty}.menuItemsSource`, + "", + ); + + return currentMenuItemsSource === menuItemsSource; +}; + +export const hideIfMenuItemsSourceDataIsFalsy = ( + props: TableWidgetProps, + propertyPath: string, +) => { + const baseProperty = getBasePropertyPath(propertyPath); + const sourceData = get(props, `${baseProperty}.sourceData`, ""); + + return !sourceData; +}; + +export const updateMenuItemsSource = ( + props: TableWidgetProps, + propertyPath: string, + propertyValue: unknown, +): Array<{ propertyPath: string; propertyValue: unknown }> | undefined => { + const propertiesToUpdate: Array<{ + propertyPath: string; + propertyValue: unknown; + }> = []; + const baseProperty = getBasePropertyPath(propertyPath); + const menuItemsSource = get(props, `${baseProperty}.menuItemsSource`); + + if (propertyValue === ColumnTypes.MENU_BUTTON && !menuItemsSource) { + // Sets the default value for menuItemsSource to static when + // selecting the menu button column type for the first time + propertiesToUpdate.push({ + propertyPath: `${baseProperty}.menuItemsSource`, + propertyValue: MenuItemsSource.STATIC, + }); + } else { + const sourceData = get(props, `${baseProperty}.sourceData`); + const configureMenuItems = get(props, `${baseProperty}.configureMenuItems`); + const isMenuItemsSourceChangedFromStaticToDynamic = + menuItemsSource === MenuItemsSource.STATIC && + propertyValue === MenuItemsSource.DYNAMIC; + + if (isMenuItemsSourceChangedFromStaticToDynamic) { + if (!sourceData) { + propertiesToUpdate.push({ + propertyPath: `${baseProperty}.sourceData`, + propertyValue: [], + }); + } + + if (!configureMenuItems) { + propertiesToUpdate.push({ + propertyPath: `${baseProperty}.configureMenuItems`, + propertyValue: { + label: "Configure Menu Items", + id: "config", + config: { + id: "config", + label: "Menu Item", + isVisible: true, + isDisabled: false, + }, + }, + }); + } + } + } + + return propertiesToUpdate?.length ? propertiesToUpdate : undefined; +}; + export function selectColumnOptionsValidation( value: unknown, props: TableWidgetProps, diff --git a/app/client/src/widgets/TableWidgetV2/widget/utilities.test.ts b/app/client/src/widgets/TableWidgetV2/widget/utilities.test.ts index c082031f8a..4fa85eb2e5 100644 --- a/app/client/src/widgets/TableWidgetV2/widget/utilities.test.ts +++ b/app/client/src/widgets/TableWidgetV2/widget/utilities.test.ts @@ -9,6 +9,7 @@ import { getOriginalRowIndex, getSelectRowIndex, getSelectRowIndices, + getSourceDataAndCaluclateKeysForEventAutoComplete, getTableStyles, reorderColumns, } from "./utilities"; @@ -1992,6 +1993,264 @@ describe("getColumnType", () => { }); }); +describe("getSourceDataAndCaluclateKeysForEventAutoComplete", () => { + it("Should test with valid values", () => { + const mockProps = { + type: "TABLE_WIDGET_V2", + widgetName: "Table1", + widgetId: "9oh3qyw84m", + primaryColumns: { + action: { + configureMenuItems: { + config: { + label: + "{{Table1.primaryColumns.action.sourceData.map((currentItem, currentIndex) => ( currentItem.))}}", + }, + }, + }, + }, + __evaluation__: { + errors: { + primaryColumns: [], + }, + evaluatedValues: { + primaryColumns: { + step: { + index: 0, + width: 150, + id: "step", + originalId: "step", + alias: "step", + columnType: "text", + label: "step", + computedValue: ["#1", "#2", "#3"], + validation: {}, + labelColor: "#FFFFFF", + }, + action: { + index: 3, + width: 150, + id: "action", + originalId: "action", + alias: "action", + columnType: "menuButton", + label: "action", + computedValue: ["", "", ""], + labelColor: "#FFFFFF", + buttonLabel: ["Action", "Action", "Action"], + menuColor: ["#553DE9", "#553DE9", "#553DE9"], + menuItemsSource: "DYNAMIC", + menuButtonLabel: ["Open Menu", "Open Menu", "Open Menu"], + sourceData: [ + { + gender: "male", + name: "Victor", + email: "victor.garrett@example.com", + }, + { + gender: "male", + name: "Tobias", + email: "tobias.hansen@example.com", + }, + { + gender: "female", + name: "Jane", + email: "jane.coleman@example.com", + }, + { + gender: "female", + name: "Yaromira", + email: "yaromira.manuylenko@example.com", + }, + { + gender: "male", + name: "Andre", + email: "andre.ortiz@example.com", + }, + ], + configureMenuItems: { + label: "Configure Menu Items", + id: "config", + config: { + id: "config", + label: ["male", "male", "female", "female", "male"], + isVisible: true, + isDisabled: false, + onClick: "", + backgroundColor: ["red", "red", "tan", "tan", "red"], + iconName: "add-row-top", + iconAlign: "right", + }, + }, + }, + }, + }, + }, + }; + + const result = getSourceDataAndCaluclateKeysForEventAutoComplete( + mockProps as any, + ); + const expected = { + currentItem: { + name: "", + email: "", + gender: "", + }, + }; + expect(result).toStrictEqual(expected); + }); + + it("Should test with empty sourceData", () => { + const mockProps = { + type: "TABLE_WIDGET_V2", + widgetName: "Table1", + widgetId: "9oh3qyw84m", + primaryColumns: { + action: { + configureMenuItems: { + config: { + label: + "{{Table1.primaryColumns.action.sourceData.map((currentItem, currentIndex) => ( currentItem.))}}", + }, + }, + }, + }, + __evaluation__: { + errors: { + primaryColumns: [], + }, + evaluatedValues: { + primaryColumns: { + step: { + index: 0, + width: 150, + id: "step", + originalId: "step", + alias: "step", + columnType: "text", + label: "step", + computedValue: ["#1", "#2", "#3"], + validation: {}, + labelColor: "#FFFFFF", + }, + action: { + index: 3, + width: 150, + id: "action", + originalId: "action", + alias: "action", + columnType: "menuButton", + label: "action", + computedValue: ["", "", ""], + labelColor: "#FFFFFF", + buttonLabel: ["Action", "Action", "Action"], + menuColor: ["#553DE9", "#553DE9", "#553DE9"], + menuItemsSource: "DYNAMIC", + menuButtonLabel: ["Open Menu", "Open Menu", "Open Menu"], + sourceData: [], + configureMenuItems: { + label: "Configure Menu Items", + id: "config", + config: { + id: "config", + label: ["male", "male", "female", "female", "male"], + isVisible: true, + isDisabled: false, + onClick: "", + backgroundColor: ["red", "red", "tan", "tan", "red"], + iconName: "add-row-top", + iconAlign: "right", + }, + }, + }, + }, + }, + }, + }; + + const result = getSourceDataAndCaluclateKeysForEventAutoComplete( + mockProps as any, + ); + const expected = { currentItem: {} }; + expect(result).toStrictEqual(expected); + }); + + it("Should test without sourceData", () => { + const mockProps = { + type: "TABLE_WIDGET_V2", + widgetName: "Table1", + widgetId: "9oh3qyw84m", + primaryColumns: { + action: { + configureMenuItems: { + config: { + label: + "{{Table1.primaryColumns.action.sourceData.map((currentItem, currentIndex) => ( currentItem.))}}", + }, + }, + }, + }, + __evaluation__: { + errors: { + primaryColumns: [], + }, + evaluatedValues: { + primaryColumns: { + step: { + index: 0, + width: 150, + id: "step", + originalId: "step", + alias: "step", + columnType: "text", + label: "step", + computedValue: ["#1", "#2", "#3"], + validation: {}, + labelColor: "#FFFFFF", + }, + action: { + index: 3, + width: 150, + id: "action", + originalId: "action", + alias: "action", + columnType: "menuButton", + label: "action", + computedValue: ["", "", ""], + labelColor: "#FFFFFF", + buttonLabel: ["Action", "Action", "Action"], + menuColor: ["#553DE9", "#553DE9", "#553DE9"], + menuItemsSource: "DYNAMIC", + menuButtonLabel: ["Open Menu", "Open Menu", "Open Menu"], + configureMenuItems: { + label: "Configure Menu Items", + id: "config", + config: { + id: "config", + label: ["male", "male", "female", "female", "male"], + isVisible: true, + isDisabled: false, + onClick: "", + backgroundColor: ["red", "red", "tan", "tan", "red"], + iconName: "add-row-top", + iconAlign: "right", + }, + }, + }, + }, + }, + }, + }; + + const result = getSourceDataAndCaluclateKeysForEventAutoComplete( + mockProps as any, + ); + const expected = { currentItem: {} }; + expect(result).toStrictEqual(expected); + }); +}); + describe("getArrayPropertyValue", () => { it("should test that it returns the same value when value is not of expected type", () => { expect(getArrayPropertyValue("test", 1)).toEqual("test"); diff --git a/app/client/src/widgets/TableWidgetV2/widget/utilities.ts b/app/client/src/widgets/TableWidgetV2/widget/utilities.ts index 0bf199d7bc..c6cac6e1eb 100644 --- a/app/client/src/widgets/TableWidgetV2/widget/utilities.ts +++ b/app/client/src/widgets/TableWidgetV2/widget/utilities.ts @@ -27,6 +27,7 @@ import { ButtonVariantTypes } from "components/constants"; import { dateFormatOptions } from "widgets/constants"; import moment from "moment"; import { Stylesheet } from "entities/AppTheming"; +import { getKeysFromSourceDataForEventAutocomplete } from "widgets/MenuButtonWidget/widget/helper"; type TableData = Array>; @@ -234,12 +235,19 @@ export const getPropertyValue = ( value: any, index: number, preserveCase = false, + isSourceData = false, ) => { if (value && isObject(value) && !Array.isArray(value)) { return value; } if (value && Array.isArray(value) && value[index]) { - return preserveCase + const getValueForSourceData = (value: any, index: number) => { + return Array.isArray(value[index]) ? value[index] : value; + }; + + return isSourceData + ? getValueForSourceData(value, index) + : preserveCase ? value[index].toString() : value[index].toString().toUpperCase(); } else if (value) { @@ -307,6 +315,18 @@ export const getCellProperties = ( rowIndex, true, ), + menuItemsSource: getPropertyValue( + columnProperties.menuItemsSource, + rowIndex, + true, + ), + sourceData: getPropertyValue( + columnProperties.sourceData, + rowIndex, + false, + true, + ), + configureMenuItems: columnProperties.configureMenuItems, buttonVariant: getPropertyValue( columnProperties.buttonVariant, rowIndex, @@ -697,3 +717,22 @@ export const getColumnType = ( return ColumnTypes.TEXT; } }; + +export const getSourceDataAndCaluclateKeysForEventAutoComplete = ( + props: TableWidgetProps, +): unknown => { + const { __evaluation__, primaryColumns } = props; + const primaryColumnKeys = primaryColumns ? Object.keys(primaryColumns) : []; + const columnName = primaryColumnKeys?.length ? primaryColumnKeys[0] : ""; + const evaluatedColumns: any = __evaluation__?.evaluatedValues?.primaryColumns; + + if (evaluatedColumns) { + const result = getKeysFromSourceDataForEventAutocomplete( + evaluatedColumns[columnName]?.sourceData || [], + ); + + return result; + } else { + return {}; + } +}; diff --git a/app/client/src/workers/Evaluation/validations.ts b/app/client/src/workers/Evaluation/validations.ts index aa18e08a68..90fc11101d 100644 --- a/app/client/src/workers/Evaluation/validations.ts +++ b/app/client/src/workers/Evaluation/validations.ts @@ -942,7 +942,7 @@ export const VALIDATORS: Record = { {}, false, undefined, - [value, props, _, moment, propertyPath], + [value, props, _, moment, propertyPath, config], ); return result; } catch (e) {