diff --git a/app/client/src/components/propertyControls/CodeEditorControl.tsx b/app/client/src/components/propertyControls/CodeEditorControl.tsx index 19f47fb09745..44b8d0d73e46 100644 --- a/app/client/src/components/propertyControls/CodeEditorControl.tsx +++ b/app/client/src/components/propertyControls/CodeEditorControl.tsx @@ -1,17 +1,17 @@ -import type { ChangeEvent } from "react"; -import React from "react"; -import type { ControlProps } from "./BaseControl"; -import BaseControl from "./BaseControl"; -import type { EventOrValueHandler } from "redux-form"; +import { slashCommandHintHelper } from "components/editorComponents/CodeEditor/commandsHelper"; import { EditorModes, EditorSize, + EditorTheme, TabBehaviour, } from "components/editorComponents/CodeEditor/EditorConfig"; -import LazyCodeEditor from "components/editorComponents/LazyCodeEditor"; import { bindingHintHelper } from "components/editorComponents/CodeEditor/hintHelpers"; -import { slashCommandHintHelper } from "components/editorComponents/CodeEditor/commandsHelper"; -import type { EditorProps } from "components/editorComponents/CodeEditor"; +import LazyCodeEditor from "components/editorComponents/LazyCodeEditor"; +import type { ChangeEvent } from "react"; +import React from "react"; +import type { EventOrValueHandler } from "redux-form"; +import type { ControlProps } from "./BaseControl"; +import BaseControl from "./BaseControl"; class CodeEditorControl extends BaseControl { render() { @@ -21,31 +21,41 @@ class CodeEditorControl extends BaseControl { evaluatedValue, expected, propertyValue, + shouldDisableSection, useValidationMessage, } = this.props; - const props: Partial = {}; + // PropertyPaneControlConfig's disabled is a function (props: any, propertyPath: string) => boolean + // while LazyCodeEditor expects a boolean. Convert function to boolean result. + const isControlDisabled = + typeof shouldDisableSection === "function" + ? shouldDisableSection( + this.props.widgetProperties, + this.props.propertyName, + ) + : !!shouldDisableSection; - if (dataTreePath) props.dataTreePath = dataTreePath; - - if (evaluatedValue) props.evaluatedValue = evaluatedValue; - - if (expected) props.expected = expected; + const maxHeight = controlConfig?.maxHeight + ? String(controlConfig.maxHeight) + : undefined; return ( ); } diff --git a/app/client/src/components/propertyControls/InputTextControl.tsx b/app/client/src/components/propertyControls/InputTextControl.tsx index 4671f88a9afc..bcc5101192cd 100644 --- a/app/client/src/components/propertyControls/InputTextControl.tsx +++ b/app/client/src/components/propertyControls/InputTextControl.tsx @@ -1,9 +1,6 @@ -import React from "react"; -import type { ControlProps } from "./BaseControl"; -import BaseControl from "./BaseControl"; -import { StyledDynamicInput } from "./StyledControls"; import type { InputType } from "components/constants"; import type { CodeEditorExpected } from "components/editorComponents/CodeEditor"; +import { slashCommandHintHelper } from "components/editorComponents/CodeEditor/commandsHelper"; import type { FieldEntityInformation } from "components/editorComponents/CodeEditor/EditorConfig"; import { CodeEditorBorder, @@ -12,11 +9,14 @@ import { EditorTheme, TabBehaviour, } from "components/editorComponents/CodeEditor/EditorConfig"; -import { CollapseContext } from "pages/Editor/PropertyPane/PropertySection"; -import LazyCodeEditor from "../editorComponents/LazyCodeEditor"; -import type { AdditionalDynamicDataTree } from "utils/autocomplete/customTreeTypeDefCreator"; import { bindingHintHelper } from "components/editorComponents/CodeEditor/hintHelpers"; -import { slashCommandHintHelper } from "components/editorComponents/CodeEditor/commandsHelper"; +import { CollapseContext } from "pages/Editor/PropertyPane/PropertyPaneContexts"; +import React from "react"; +import type { AdditionalDynamicDataTree } from "utils/autocomplete/customTreeTypeDefCreator"; +import LazyCodeEditor from "../editorComponents/LazyCodeEditor"; +import type { ControlProps } from "./BaseControl"; +import BaseControl from "./BaseControl"; +import { StyledDynamicInput } from "./StyledControls"; export function InputText(props: { label: string; diff --git a/app/client/src/components/propertyControls/TableCustomSortControl.tsx b/app/client/src/components/propertyControls/TableCustomSortControl.tsx index 5c9338407c25..a275ab037054 100644 --- a/app/client/src/components/propertyControls/TableCustomSortControl.tsx +++ b/app/client/src/components/propertyControls/TableCustomSortControl.tsx @@ -1,11 +1,8 @@ -import React from "react"; -import type { ControlProps } from "./BaseControl"; -import BaseControl from "./BaseControl"; -import { StyledDynamicInput } from "./StyledControls"; import type { CodeEditorExpected, EditorProps, } from "components/editorComponents/CodeEditor"; +import { slashCommandHintHelper } from "components/editorComponents/CodeEditor/commandsHelper"; import { CodeEditorBorder, EditorModes, @@ -13,16 +10,19 @@ import { EditorTheme, TabBehaviour, } from "components/editorComponents/CodeEditor/EditorConfig"; -import type { ColumnProperties } from "widgets/TableWidgetV2/component/Constants"; -import { isDynamicValue } from "utils/DynamicBindingUtils"; +import { bindingHintHelper } from "components/editorComponents/CodeEditor/hintHelpers"; +import LazyCodeEditor from "components/editorComponents/LazyCodeEditor"; +import { CollapseContext } from "pages/Editor/PropertyPane/PropertyPaneContexts"; +import React from "react"; import styled from "styled-components"; +import type { AdditionalDynamicDataTree } from "utils/autocomplete/customTreeTypeDefCreator"; +import { isDynamicValue } from "utils/DynamicBindingUtils"; import { isString } from "utils/helpers"; +import type { ColumnProperties } from "widgets/TableWidgetV2/component/Constants"; +import type { ControlProps } from "./BaseControl"; +import BaseControl from "./BaseControl"; +import { StyledDynamicInput } from "./StyledControls"; import { JSToString, stringToJS } from "./utils"; -import type { AdditionalDynamicDataTree } from "utils/autocomplete/customTreeTypeDefCreator"; -import LazyCodeEditor from "components/editorComponents/LazyCodeEditor"; -import { bindingHintHelper } from "components/editorComponents/CodeEditor/hintHelpers"; -import { slashCommandHintHelper } from "components/editorComponents/CodeEditor/commandsHelper"; -import { CollapseContext } from "pages/Editor/PropertyPane/PropertySection"; const PromptMessage = styled.span` line-height: 17px; diff --git a/app/client/src/constants/PropertyControlConstants.tsx b/app/client/src/constants/PropertyControlConstants.tsx index 50b5964f86cf..43b3e0b1fdb7 100644 --- a/app/client/src/constants/PropertyControlConstants.tsx +++ b/app/client/src/constants/PropertyControlConstants.tsx @@ -53,6 +53,19 @@ export interface PropertyPaneSectionConfig { // TODO: Fix this the next time the file is edited // eslint-disable-next-line @typescript-eslint/no-explicit-any hidden?: (props: any, propertyPath: string) => boolean; + /** + * @param props - Current widget properties + * @param propertyPath - Path to the widget property + * @returns - True if the section should be disabled, false otherwise + */ + // TODO: Fix this the next time the file is edited + // eslint-disable-next-line @typescript-eslint/no-explicit-any + shouldDisableSection?: (props: any, propertyPath: string) => boolean; + /** + * Help text to show when section is disabled. + * Appears as a tooltip when hovering over the disabled section. + */ + disabledHelpText?: string; /** * when true, the section will be open by default. * Note: Seems like this is not used anywhere. @@ -205,6 +218,20 @@ export interface PropertyPaneControlConfig { // TODO: Fix this the next time the file is edited // eslint-disable-next-line @typescript-eslint/no-explicit-any hidden?: (props: any, propertyPath: string) => boolean; + /** + * callback function to determine if the property should be disabled. + + * @param props - Current widget properties + * @param propertyPath - Path to the widget property + * @returns - True if the property should be disabled, false otherwise + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + shouldDisableSection?: (props: any, propertyPath: string) => boolean; + /** + * Help text to show when property is disabled. + * Appears as a tooltip when hovering over the disabled property. + */ + disabledHelpText?: string; /** * If true, the property is hidden. * Note: hidden and invisible do the same thing but differently. hidden uses a callback to determine if the property should be hidden. diff --git a/app/client/src/pages/Editor/PropertyPane/PropertyControl.test.tsx b/app/client/src/pages/Editor/PropertyPane/PropertyControl.test.tsx new file mode 100644 index 000000000000..b3342479d030 --- /dev/null +++ b/app/client/src/pages/Editor/PropertyPane/PropertyControl.test.tsx @@ -0,0 +1,124 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig"; +import type { IPanelProps } from "@blueprintjs/core"; +import type { WidgetProps } from "widgets/BaseWidget"; +import PropertyControl from "./PropertyControl"; +import type { EnhancementFns } from "selectors/widgetEnhancementSelectors"; + +interface MockPropertyControlProps { + shouldDisableSection?: ( + widgetProperties: WidgetProps, + propertyName: string, + ) => boolean; + disabledHelpText?: string; + label: string; + propertyName: string; + widgetProperties: WidgetProps; +} + +const MockPropertyControl = (props: MockPropertyControlProps) => { + const isDisabled = props.shouldDisableSection + ? props.shouldDisableSection(props.widgetProperties, props.propertyName) + : false; + + return ( +
+ + + {isDisabled && props.disabledHelpText && ( +
{props.disabledHelpText}
+ )} +
+ ); +}; + +jest.mock("./PropertyControl", () => (props: MockPropertyControlProps) => ( + +)); + +describe("PropertyControl", () => { + const mockPanel: IPanelProps = { + closePanel: jest.fn(), + openPanel: jest.fn(), + }; + + const defaultProps = { + controlType: "INPUT_TEXT", + enhancements: undefined as EnhancementFns | undefined, + isBindProperty: true, + isSearchResult: false, + isTriggerProperty: false, + label: "Test Label", + panel: mockPanel, + propertyName: "testProperty", + theme: EditorTheme.LIGHT, + widgetProperties: { + testProperty: "test value", + type: "CONTAINER_WIDGET", + widgetId: "test-widget", + widgetName: "TestWidget", + }, + }; + + it("should render property control normally when not disabled", () => { + const { getByTestId } = render(); + + expect( + (getByTestId("t--property-input") as HTMLInputElement).disabled, + ).toBe(false); + expect(getByTestId("t--property-control-wrapper").className).toBe(""); + }); + + it("should render disabled property control when disabled prop is true", () => { + const disabledProps = { + ...defaultProps, + shouldDisableSection: () => true, + }; + + const { getByTestId } = render(); + + const wrapper = getByTestId("t--property-control-wrapper"); + + expect(wrapper.classList.contains("cursor-not-allowed")).toBe(true); + expect(wrapper.classList.contains("opacity-50")).toBe(true); + expect( + (getByTestId("t--property-input") as HTMLInputElement).disabled, + ).toBe(true); + }); + + it("should show disabled help text when property is disabled", () => { + const disabledProps = { + ...defaultProps, + shouldDisableSection: () => true, + disabledHelpText: "This property is disabled because...", + }; + + const { getByTestId } = render(); + + expect(getByTestId("t--disabled-help-text")).toBeTruthy(); + expect(getByTestId("t--disabled-help-text").textContent).toBe( + "This property is disabled because...", + ); + }); + + it("should not show disabled help text when property is not disabled", () => { + const props = { + ...defaultProps, + shouldDisableSection: () => false, + disabledHelpText: "This property is disabled because...", + }; + + const { queryByTestId } = render(); + + expect(queryByTestId("t--disabled-help-text")).toBeFalsy(); + }); +}); diff --git a/app/client/src/pages/Editor/PropertyPane/PropertyControl.tsx b/app/client/src/pages/Editor/PropertyPane/PropertyControl.tsx index e27b2b80c060..cdd4bff5c885 100644 --- a/app/client/src/pages/Editor/PropertyPane/PropertyControl.tsx +++ b/app/client/src/pages/Editor/PropertyPane/PropertyControl.tsx @@ -1,14 +1,11 @@ -import React, { memo, useCallback, useEffect, useRef, useState } from "react"; -import _, { get, isFunction, merge } from "lodash"; import equal from "fast-deep-equal/es6"; +import _, { get, isFunction, merge } from "lodash"; import * as log from "loglevel"; +import React, { memo, useCallback, useEffect, useRef, useState } from "react"; -import { ControlWrapper } from "components/propertyControls/StyledControls"; -import { ToggleButton, Tooltip, Button } from "@appsmith/ads"; -import PropertyControlFactory from "utils/PropertyControlFactory"; -import PropertyHelpLabel from "pages/Editor/PropertyPane/PropertyHelpLabel"; -import { useDispatch, useSelector } from "react-redux"; -import AnalyticsUtil from "ee/utils/AnalyticsUtil"; +import { Button, ToggleButton, Tooltip } from "@appsmith/ads"; +import { importSvg } from "@appsmith/ads-old"; +import type { IPanelProps } from "@blueprintjs/core"; import type { UpdateWidgetPropertyPayload } from "actions/controlActions"; import { batchUpdateMultipleWidgetProperties, @@ -16,54 +13,57 @@ import { deleteWidgetProperty, setWidgetDynamicProperty, } from "actions/controlActions"; +import { + setFocusablePropertyPaneField, + setSelectedPropertyPanel, +} from "actions/propertyPaneActions"; +import classNames from "classnames"; +import clsx from "clsx"; +import type { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig"; +import type { ControlData } from "components/propertyControls/BaseControl"; +import { ControlWrapper } from "components/propertyControls/StyledControls"; import type { PropertyPaneControlConfig } from "constants/PropertyControlConstants"; -import type { IPanelProps } from "@blueprintjs/core"; -import PanelPropertiesEditor from "./PanelPropertiesEditor"; -import type { DynamicPath } from "utils/DynamicBindingUtils"; import { - getEvalValuePath, - isDynamicValue, - THEME_BINDING_REGEX, -} from "utils/DynamicBindingUtils"; + JS_TOGGLE_DISABLED_MESSAGE, + JS_TOGGLE_SWITCH_JS_MESSAGE, +} from "ee/constants/messages"; +import { ENTITY_TYPE } from "ee/entities/AppsmithConsole/utils"; +import { FEATURE_FLAG } from "ee/entities/FeatureFlag"; +import type { AppState } from "ee/reducers"; +import AnalyticsUtil from "ee/utils/AnalyticsUtil"; +import LOG_TYPE from "entities/AppsmithConsole/logtype"; +import PropertyHelpLabel from "pages/Editor/PropertyPane/PropertyHelpLabel"; +import { useDispatch, useSelector } from "react-redux"; +import { getIsOneClickBindingOptionsVisibility } from "selectors/oneClickBindingSelectors"; import type { WidgetProperties } from "selectors/propertyPaneSelectors"; import { getShouldFocusPropertyPath, getWidgetPropsForPropertyName, } from "selectors/propertyPaneSelectors"; import type { EnhancementFns } from "selectors/widgetEnhancementSelectors"; -import type { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig"; +import { getParentWidget } from "selectors/widgetSelectors"; +import styled from "styled-components"; import AppsmithConsole from "utils/AppsmithConsole"; -import { ENTITY_TYPE } from "ee/entities/AppsmithConsole/utils"; -import LOG_TYPE from "entities/AppsmithConsole/logtype"; -import { getExpectedValue } from "utils/validation/common"; -import type { ControlData } from "components/propertyControls/BaseControl"; -import type { AppState } from "ee/reducers"; import { AutocompleteDataType } from "utils/autocomplete/AutocompleteDataType"; +import type { AdditionalDynamicDataTree } from "utils/autocomplete/customTreeTypeDefCreator"; +import type { DynamicPath } from "utils/DynamicBindingUtils"; import { - JS_TOGGLE_DISABLED_MESSAGE, - JS_TOGGLE_SWITCH_JS_MESSAGE, -} from "ee/constants/messages"; + getEvalValuePath, + isDynamicValue, + THEME_BINDING_REGEX, +} from "utils/DynamicBindingUtils"; import { getPropertyControlFocusElement, shouldFocusOnPropertyControl, } from "utils/editorContextUtils"; -import PropertyPaneHelperText from "./PropertyPaneHelperText"; -import { - setFocusablePropertyPaneField, - setSelectedPropertyPanel, -} from "actions/propertyPaneActions"; -import WidgetFactory from "WidgetProvider/factory"; -import type { AdditionalDynamicDataTree } from "utils/autocomplete/customTreeTypeDefCreator"; -import clsx from "clsx"; -import styled from "styled-components"; -import { importSvg } from "@appsmith/ads-old"; -import classNames from "classnames"; -import type { PropertyUpdates } from "WidgetProvider/constants"; -import { getIsOneClickBindingOptionsVisibility } from "selectors/oneClickBindingSelectors"; import { useFeatureFlag } from "utils/hooks/useFeatureFlag"; -import { FEATURE_FLAG } from "ee/entities/FeatureFlag"; +import PropertyControlFactory from "utils/PropertyControlFactory"; +import { getExpectedValue } from "utils/validation/common"; +import type { PropertyUpdates } from "WidgetProvider/constants"; +import WidgetFactory from "WidgetProvider/factory"; import { savePropertyInSessionStorageIfRequired } from "./helpers"; -import { getParentWidget } from "selectors/widgetSelectors"; +import PanelPropertiesEditor from "./PanelPropertiesEditor"; +import PropertyPaneHelperText from "./PropertyPaneHelperText"; const ResetIcon = importSvg( async () => import("assets/icons/control/undo_2.svg"), @@ -105,6 +105,10 @@ const PropertyControl = memo((props: Props) => { getParentWidget(state, widgetProperties.widgetId), ); + const isControlDisabled = + props.shouldDisableSection && + props.shouldDisableSection(widgetProperties, props.propertyName); + // get the dataTreePath and apply enhancement if exists let dataTreePath: string | undefined = props.dataTreePath || widgetProperties @@ -882,8 +886,9 @@ const PropertyControl = memo((props: Props) => { } } - const helpText = - config.controlType === "ACTION_SELECTOR" + const helpText = isControlDisabled + ? props.disabledHelpText || "" + : config.controlType === "ACTION_SELECTOR" ? `Configure one or chain multiple actions. ${props.helpText}. All nested actions run at the same time.` : props.helpText; @@ -916,7 +921,7 @@ const PropertyControl = memo((props: Props) => { try { return (