From 9e6417d88248af035574426500fed4b498a290af Mon Sep 17 00:00:00 2001 From: jacquesikot Date: Wed, 26 Mar 2025 02:22:20 +0100 Subject: [PATCH 01/32] feat: show infinite scroll only when serverSidePagination is enabled --- .../TableWidgetV2/widget/propertyConfig/contentConfig.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/client/src/widgets/TableWidgetV2/widget/propertyConfig/contentConfig.ts b/app/client/src/widgets/TableWidgetV2/widget/propertyConfig/contentConfig.ts index 6bbb90486aba..fa09ccb94ca9 100644 --- a/app/client/src/widgets/TableWidgetV2/widget/propertyConfig/contentConfig.ts +++ b/app/client/src/widgets/TableWidgetV2/widget/propertyConfig/contentConfig.ts @@ -179,13 +179,16 @@ export default [ }, { helpText: - "Bind the Table.pageNo property in your API and call it onPageChange", + "Enables load more data as the user scrolls down", propertyName: "infiniteScrollEnabled", label: "Infinite scroll", controlType: "SWITCH", isBindProperty: false, isTriggerProperty: false, - hidden: () => !Widget.getFeatureFlag(INFINITE_SCROLL_ENABLED), + hidden: (props: TableWidgetProps) => + !props.serverSidePaginationEnabled || + !Widget.getFeatureFlag(INFINITE_SCROLL_ENABLED), + dependencies: ["serverSidePaginationEnabled"], }, { helpText: createMessage(TABLE_WIDGET_TOTAL_RECORD_TOOLTIP), From 86bd4b03a47a45bd5114b7d3dbff819e727fd00a Mon Sep 17 00:00:00 2001 From: jacquesikot Date: Wed, 26 Mar 2025 11:59:35 +0100 Subject: [PATCH 02/32] feat: Add disabled state and disabled help text for property sections and controls --- .../constants/PropertyControlConstants.tsx | 26 ++ .../Editor/PropertyPane/PropertyControl.tsx | 423 +++++++++--------- .../PropertyControlsGenerator.tsx | 2 + .../Editor/PropertyPane/PropertySection.tsx | 101 +++-- .../src/pages/Editor/PropertyPane/helpers.tsx | 7 +- 5 files changed, 309 insertions(+), 250 deletions(-) diff --git a/app/client/src/constants/PropertyControlConstants.tsx b/app/client/src/constants/PropertyControlConstants.tsx index 50b5964f86cf..501928a3d013 100644 --- a/app/client/src/constants/PropertyControlConstants.tsx +++ b/app/client/src/constants/PropertyControlConstants.tsx @@ -53,6 +53,18 @@ 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 + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + disabled?: (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 +217,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 + disabled?: (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.tsx b/app/client/src/pages/Editor/PropertyPane/PropertyControl.tsx index e27b2b80c060..7fdecfc20c1a 100644 --- a/app/client/src/pages/Editor/PropertyPane/PropertyControl.tsx +++ b/app/client/src/pages/Editor/PropertyPane/PropertyControl.tsx @@ -1,4 +1,4 @@ -import React, { memo, useCallback, useEffect, useRef, useState } from "react"; +import React, { memo, useCallback, useContext, useEffect, useRef, useState } from "react"; import _, { get, isFunction, merge } from "lodash"; import equal from "fast-deep-equal/es6"; import * as log from "loglevel"; @@ -64,6 +64,7 @@ import { useFeatureFlag } from "utils/hooks/useFeatureFlag"; import { FEATURE_FLAG } from "ee/entities/FeatureFlag"; import { savePropertyInSessionStorageIfRequired } from "./helpers"; import { getParentWidget } from "selectors/widgetSelectors"; +import { DisabledContext } from "./PropertySection"; const ResetIcon = importSvg( async () => import("assets/icons/control/undo_2.svg"), @@ -89,6 +90,8 @@ const SHOULD_NOT_REJECT_DYNAMIC_BINDING_LIST_FOR = ["COLOR_PICKER"]; const PropertyControl = memo((props: Props) => { const dispatch = useDispatch(); + // Get disabled state from context (from parent section if available) + const isSectionDisabled = useContext(DisabledContext); const controlRef = useRef(null); const [showEmptyBlock, setShowEmptyBlock] = React.useState(false); @@ -105,6 +108,10 @@ const PropertyControl = memo((props: Props) => { getParentWidget(state, widgetProperties.widgetId), ); + const isControlDisabled = props.disabled && props.disabled(widgetProperties, props.propertyName); + + const isDisabled = isSectionDisabled || !!isControlDisabled; + // get the dataTreePath and apply enhancement if exists let dataTreePath: string | undefined = props.dataTreePath || widgetProperties @@ -882,8 +889,9 @@ const PropertyControl = memo((props: Props) => { } } - const helpText = - config.controlType === "ACTION_SELECTOR" + const helpText = isDisabled + ? props.disabledHelpText || "" + : config.controlType === "ACTION_SELECTOR" ? `Configure one or chain multiple actions. ${props.helpText}. All nested actions run at the same time.` : props.helpText; @@ -914,235 +922,236 @@ const PropertyControl = memo((props: Props) => { : ""; try { - return ( - - {isRenaming && config.controlConfig?.allowEdit ? ( -
-
- { - const value = e.target.value; - - // Non-word characters are replaced with underscores for valid property naming - setEditedName(value.split(/\W+/).join("_")); - }} - onKeyDown={(e) => { - if (e.key === "Enter") { - onEditSave(); - } else if (e.key === "Escape") { - resetEditing(); - } - }} - placeholder="Enter label" - value={editedName} - /> -
-
-
-
-
-
- ) : ( -
- - - {isConvertible && ( - - - - toggleDynamicProperty( - propertyName, - isDynamic, - controlMethods?.shouldValidateValueOnDynamicPropertyOff( - config, - propertyValue, - ), - ) - } - size={experimentalJSToggle ? "md" : "sm"} - /> - - - )} - {isPropertyDeviatedFromTheme && ( - <> - - - - - - )} - -
- {config.controlConfig?.allowEdit && ( + const controlWrapper = ( + + {isRenaming && config.controlConfig?.allowEdit ? ( +
+
+ { + const value = e.target.value; + + // Non-word characters are replaced with underscores for valid property naming + setEditedName(value.split(/\W+/).join("_")); + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + onEditSave(); + } else if (e.key === "Escape") { + resetEditing(); + } + }} + placeholder="Enter label" + value={editedName} + /> +
+
+
+
+ ) : ( +
+ + - )} + {isConvertible && ( + + + + toggleDynamicProperty( + propertyName, + isDynamic, + controlMethods?.shouldValidateValueOnDynamicPropertyOff( + config, + propertyValue, + ), + ) + } + size={experimentalJSToggle ? "md" : "sm"} + /> + + + )} + {isPropertyDeviatedFromTheme && ( + <> + + + + + + )} + +
+ {config.controlConfig?.allowEdit && ( +
-
- )} - {PropertyControlFactory.createControl( - config, - { - onPropertyChange: onPropertyChange, - onBatchUpdateProperties: onBatchUpdateProperties, - openNextPanel: openPanel, - deleteProperties: onDeleteProperties, - onBatchUpdateWithAssociatedUpdates: - onBatchUpdateWithAssociatedWidgetUpdates, - theme: props.theme, - }, - isDynamic, - customJSControl, - additionAutocomplete, - hideEvaluatedValue(), - props.isSearchResult, - )} - - + )} + {PropertyControlFactory.createControl( + config, + { + onPropertyChange: onPropertyChange, + onBatchUpdateProperties: onBatchUpdateProperties, + openNextPanel: openPanel, + deleteProperties: onDeleteProperties, + onBatchUpdateWithAssociatedUpdates: + onBatchUpdateWithAssociatedWidgetUpdates, + theme: props.theme, + }, + isDynamic, + customJSControl, + additionAutocomplete, + hideEvaluatedValue(), + props.isSearchResult, + )} + + ); + return controlWrapper } catch (e) { log.error(e); diff --git a/app/client/src/pages/Editor/PropertyPane/PropertyControlsGenerator.tsx b/app/client/src/pages/Editor/PropertyPane/PropertyControlsGenerator.tsx index 4b0452cb9b4f..32f000bbb1c5 100644 --- a/app/client/src/pages/Editor/PropertyPane/PropertyControlsGenerator.tsx +++ b/app/client/src/pages/Editor/PropertyPane/PropertyControlsGenerator.tsx @@ -60,6 +60,8 @@ const generatePropertyControl = ( childrenId={sectionConfig.childrenId} collapsible={sectionConfig.collapsible ?? true} hidden={sectionConfig.hidden} + disabled={sectionConfig.disabled} + disabledHelpText={sectionConfig.disabledHelpText} id={config.id || sectionConfig.sectionName} isDefaultOpen={shouldSectionBeExpanded( sectionConfig, diff --git a/app/client/src/pages/Editor/PropertyPane/PropertySection.tsx b/app/client/src/pages/Editor/PropertyPane/PropertySection.tsx index 7b1dfbafe129..279ebc8b0901 100644 --- a/app/client/src/pages/Editor/PropertyPane/PropertySection.tsx +++ b/app/client/src/pages/Editor/PropertyPane/PropertySection.tsx @@ -10,7 +10,7 @@ import React, { import { Collapse } from "@blueprintjs/core"; import styled from "styled-components"; import { Colors } from "constants/Colors"; -import { Icon, Tag } from "@appsmith/ads"; +import { Icon, Tag, Tooltip } from "@appsmith/ads"; import type { AppState } from "ee/reducers"; import { useDispatch, useSelector } from "react-redux"; import { getPropertySectionState } from "selectors/editorContextSelectors"; @@ -20,6 +20,7 @@ import { getIsOneClickBindingOptionsVisibility } from "selectors/oneClickBinding import localStorage from "utils/localStorage"; import { WIDGET_ID_SHOW_WALKTHROUGH } from "constants/WidgetConstants"; import { PROPERTY_PANE_ID } from "components/editorComponents/PropertyPaneSidebar"; +import { getWidgetPropsForPropertyPane } from "selectors/propertyPaneSelectors"; const TagContainer = styled.div``; @@ -83,6 +84,9 @@ interface PropertySectionProps { // 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; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + disabled?: (props: any, propertyPath: string) => boolean; + disabledHelpText?: string; isDefaultOpen?: boolean; propertyPath?: string; tag?: string; // Used to show a tag on the section title on search results @@ -93,8 +97,9 @@ const areEqual = (prev: PropertySectionProps, next: PropertySectionProps) => { return prev.id === next.id && prev.childrenId === next.childrenId; }; -//Context is being provided to re-render anything that subscribes to this context on open and close +// Context is being provided to re-render anything that subscribes to this context on open and close export const CollapseContext: Context = createContext(false); +export const DisabledContext: Context = createContext(false); export const PropertySection = memo((props: PropertySectionProps) => { const dispatch = useDispatch(); @@ -108,6 +113,9 @@ export const PropertySection = memo((props: PropertySectionProps) => { ); const isSearchResult = props.tag !== undefined; const [isOpen, setIsOpen] = useState(!!isContextOpen); + + const widgetProps = useSelector(getWidgetPropsForPropertyPane); + const isSectionDisabled = props.disabled && props.disabled(widgetProps, props.propertyPath || ""); const className = props.name.split(" ").join("").toLowerCase(); const connectDataClicked = useSelector(getIsOneClickBindingOptionsVisibility); @@ -176,49 +184,60 @@ export const PropertySection = memo((props: PropertySectionProps) => { if (!currentWidgetId) return null; - return ( + const sectionContent = ( +
-
- {props.name} - {props.tag && ( - - - {props.tag.toLowerCase()} - - - )} - {props.collapsible && ( - - )} -
- {props.children && ( - -
{props.name} + {props.tag && ( + + - - {props.children} - -
-
+ {props.tag.toLowerCase()} + + + )} + {props.collapsible && !isSectionDisabled && ( + )} - +
+ {props.children && ( + +
+ + {props.children} + +
+
+ )} +
+ ) + + return ( + isSectionDisabled && props.disabledHelpText ? ( + + {sectionContent} + + ) : ( + sectionContent + ) + ); }, areEqual); diff --git a/app/client/src/pages/Editor/PropertyPane/helpers.tsx b/app/client/src/pages/Editor/PropertyPane/helpers.tsx index 4a1bf5e1769c..50f4f4ca81e1 100644 --- a/app/client/src/pages/Editor/PropertyPane/helpers.tsx +++ b/app/client/src/pages/Editor/PropertyPane/helpers.tsx @@ -60,11 +60,14 @@ export function evaluateHiddenProperty( ); if (children.length > 0) { - finalConfig.push({ + // Pass through the disabled property if it exists + const sectionWithChildren = { ...sectionConfig, childrenId: children.map((configItem) => configItem.id).join(""), children, - }); + }; + + finalConfig.push(sectionWithChildren); } } } else if (controlConfig.controlType) { From 6132745745b4c37b396ae57a3bc3e0266912a01e Mon Sep 17 00:00:00 2001 From: jacquesikot Date: Wed, 26 Mar 2025 12:05:03 +0100 Subject: [PATCH 03/32] Revert "feat: show infinite scroll only when serverSidePagination is enabled" This reverts commit 9e6417d88248af035574426500fed4b498a290af. --- .../TableWidgetV2/widget/propertyConfig/contentConfig.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/client/src/widgets/TableWidgetV2/widget/propertyConfig/contentConfig.ts b/app/client/src/widgets/TableWidgetV2/widget/propertyConfig/contentConfig.ts index fa09ccb94ca9..6bbb90486aba 100644 --- a/app/client/src/widgets/TableWidgetV2/widget/propertyConfig/contentConfig.ts +++ b/app/client/src/widgets/TableWidgetV2/widget/propertyConfig/contentConfig.ts @@ -179,16 +179,13 @@ export default [ }, { helpText: - "Enables load more data as the user scrolls down", + "Bind the Table.pageNo property in your API and call it onPageChange", propertyName: "infiniteScrollEnabled", label: "Infinite scroll", controlType: "SWITCH", isBindProperty: false, isTriggerProperty: false, - hidden: (props: TableWidgetProps) => - !props.serverSidePaginationEnabled || - !Widget.getFeatureFlag(INFINITE_SCROLL_ENABLED), - dependencies: ["serverSidePaginationEnabled"], + hidden: () => !Widget.getFeatureFlag(INFINITE_SCROLL_ENABLED), }, { helpText: createMessage(TABLE_WIDGET_TOTAL_RECORD_TOOLTIP), From 19c3620d22fd616240a528fd7f9577063b50d3fe Mon Sep 17 00:00:00 2001 From: jacquesikot Date: Wed, 26 Mar 2025 12:10:16 +0100 Subject: [PATCH 04/32] refactor: Simplify PropertyControl component structure --- .../Editor/PropertyPane/PropertyControl.tsx | 445 +++++++++--------- 1 file changed, 222 insertions(+), 223 deletions(-) diff --git a/app/client/src/pages/Editor/PropertyPane/PropertyControl.tsx b/app/client/src/pages/Editor/PropertyPane/PropertyControl.tsx index 7fdecfc20c1a..c103a84553ca 100644 --- a/app/client/src/pages/Editor/PropertyPane/PropertyControl.tsx +++ b/app/client/src/pages/Editor/PropertyPane/PropertyControl.tsx @@ -922,236 +922,235 @@ const PropertyControl = memo((props: Props) => { : ""; try { - const controlWrapper = ( - - {isRenaming && config.controlConfig?.allowEdit ? ( -
-
- { - const value = e.target.value; - - // Non-word characters are replaced with underscores for valid property naming - setEditedName(value.split(/\W+/).join("_")); - }} - onKeyDown={(e) => { - if (e.key === "Enter") { - onEditSave(); - } else if (e.key === "Escape") { - resetEditing(); - } - }} - placeholder="Enter label" - value={editedName} - /> -
-
-
-
-
-
- ) : ( -
- + {isRenaming && config.controlConfig?.allowEdit ? ( +
+
+ { + const value = e.target.value; + + // Non-word characters are replaced with underscores for valid property naming + setEditedName(value.split(/\W+/).join("_")); + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + onEditSave(); + } else if (e.key === "Escape") { + resetEditing(); } + }} + placeholder="Enter label" + value={editedName} + /> +
+
+
+
+
+
+ ) : ( +
+ + + {isConvertible && ( + - - {isConvertible && ( - - - - toggleDynamicProperty( - propertyName, - isDynamic, - controlMethods?.shouldValidateValueOnDynamicPropertyOff( - config, - propertyValue, - ), - ) - } - size={experimentalJSToggle ? "md" : "sm"} - /> - - - )} - {isPropertyDeviatedFromTheme && ( - <> - - - - - - )} - -
- {config.controlConfig?.allowEdit && ( - + + )} + +
+ {config.controlConfig?.allowEdit && ( +
- )} - {PropertyControlFactory.createControl( - config, - { - onPropertyChange: onPropertyChange, - onBatchUpdateProperties: onBatchUpdateProperties, - openNextPanel: openPanel, - deleteProperties: onDeleteProperties, - onBatchUpdateWithAssociatedUpdates: - onBatchUpdateWithAssociatedWidgetUpdates, - theme: props.theme, - }, - isDynamic, - customJSControl, - additionAutocomplete, - hideEvaluatedValue(), - props.isSearchResult, + isIconButton + kind="tertiary" + onClick={() => setShowEmptyBlock(true)} + startIcon="plus" + /> )} - - - ); - return controlWrapper +
+
+ )} + {PropertyControlFactory.createControl( + config, + { + onPropertyChange: onPropertyChange, + onBatchUpdateProperties: onBatchUpdateProperties, + openNextPanel: openPanel, + deleteProperties: onDeleteProperties, + onBatchUpdateWithAssociatedUpdates: + onBatchUpdateWithAssociatedWidgetUpdates, + theme: props.theme, + }, + isDynamic, + customJSControl, + additionAutocomplete, + hideEvaluatedValue(), + props.isSearchResult, + )} + + + ) } catch (e) { log.error(e); From 7a3e6ccc59d18df78b57ecdec641e34a8491a647 Mon Sep 17 00:00:00 2001 From: jacquesikot Date: Wed, 26 Mar 2025 13:13:57 +0100 Subject: [PATCH 05/32] test: add tests for disabled and disabledHelpText implementation --- .../PropertyPane/PropertyControl.test.tsx | 99 +++++++++++++ .../PropertyPane/PropertySection.test.tsx | 139 ++++++++++++++++++ 2 files changed, 238 insertions(+) create mode 100644 app/client/src/pages/Editor/PropertyPane/PropertyControl.test.tsx create mode 100644 app/client/src/pages/Editor/PropertyPane/PropertySection.test.tsx 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..fe0c2826077e --- /dev/null +++ b/app/client/src/pages/Editor/PropertyPane/PropertyControl.test.tsx @@ -0,0 +1,99 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig"; +import type { IPanelProps } from "@blueprintjs/core"; + +const MockPropertyControl = (props: any) => { + const isDisabled = props.disabled ? props.disabled(props.widgetProperties, props.propertyName) : false; + + return ( +
+ + + {isDisabled && props.disabledHelpText && ( +
{props.disabledHelpText}
+ )} +
+ ); +}; + +jest.mock("./PropertyControl", () => (props: any) => ); + +describe("PropertyControl", () => { + const mockPanel: IPanelProps = { + closePanel: jest.fn(), + openPanel: jest.fn(), + }; + + const defaultProps = { + propertyName: "testProperty", + label: "Test Label", + controlType: "INPUT_TEXT", + isBindProperty: true, + isTriggerProperty: false, + panel: mockPanel, + theme: EditorTheme.LIGHT, + isSearchResult: false, + widgetProperties: { + widgetId: "test-widget", + widgetName: "TestWidget", + type: "CONTAINER_WIDGET", + testProperty: "test value" + } + }; + + it("should render property control normally when not disabled", () => { + const PropertyControl = require("./PropertyControl"); + const { getByTestId } = render(); + + expect((getByTestId("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 PropertyControl = require("./PropertyControl"); + const disabledProps = { + ...defaultProps, + disabled: () => 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("property-input") as HTMLInputElement).disabled).toBe(true); + }); + + it("should show disabled help text when property is disabled", () => { + const PropertyControl = require("./PropertyControl"); + const disabledProps = { + ...defaultProps, + disabled: () => true, + disabledHelpText: "This property is disabled because...", + }; + + const { getByTestId } = render(); + + expect(getByTestId("disabled-help-text")).toBeTruthy(); + expect(getByTestId("disabled-help-text").textContent).toBe("This property is disabled because..."); + }); + + it("should not show disabled help text when property is not disabled", () => { + const PropertyControl = require("./PropertyControl"); + const props = { + ...defaultProps, + disabled: () => false, + disabledHelpText: "This property is disabled because...", + }; + + const { queryByTestId } = render(); + + expect(queryByTestId("disabled-help-text")).toBeFalsy(); + }); +}); \ No newline at end of file diff --git a/app/client/src/pages/Editor/PropertyPane/PropertySection.test.tsx b/app/client/src/pages/Editor/PropertyPane/PropertySection.test.tsx new file mode 100644 index 000000000000..9e8411be3108 --- /dev/null +++ b/app/client/src/pages/Editor/PropertyPane/PropertySection.test.tsx @@ -0,0 +1,139 @@ +import React from "react"; +import { render } from "@testing-library/react"; + + +const MockPropertySection = (props: any) => { + const isSectionDisabled = props.disabled && props.disabled(props.widgetProps, props.propertyPath || ""); + + return ( +
+
+ {props.name} + {props.children && ( +
+ {props.children} +
+ )} +
+ + {isSectionDisabled && props.disabledHelpText && ( +
{props.disabledHelpText}
+ )} +
+ ); +}; + +jest.mock("./PropertySection", () => (props: any) => ); + +describe("PropertySection", () => { + const mockOnToggle = jest.fn(); + + const getDefaultProps = () => ({ + id: "test-section", + name: "Test Section", + collapsible: true, + isDefaultOpen: true, + propertyPath: "testProperty", + widgetProps: { + widgetId: "test-widget", + widgetName: "TestWidget", + type: "CONTAINER_WIDGET", + testProperty: "test value" + }, + onToggle: mockOnToggle + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should render section normally when not disabled", () => { + const PropertySection = require("./PropertySection"); + const { getByTestId } = render(); + + const wrapper = getByTestId("t--property-pane-section-wrapper"); + expect(wrapper.className).not.toContain("cursor-not-allowed"); + expect(wrapper.className).not.toContain("opacity-50"); + + const sectionTitle = getByTestId("section-title"); + expect(sectionTitle.className).toContain("cursor-pointer"); + }); + + it("should render disabled section when disabled prop is true", () => { + const PropertySection = require("./PropertySection"); + const props = { + ...getDefaultProps(), + disabled: () => true + }; + + const { getByTestId } = render(); + + const wrapper = getByTestId("t--property-pane-section-wrapper"); + expect(wrapper.classList.contains("cursor-not-allowed")).toBe(true); + expect(wrapper.classList.contains("opacity-50")).toBe(true); + + const sectionTitle = getByTestId("section-title"); + expect(sectionTitle.className).toContain("cursor-default"); + }); + + it("should show disabled help text when section is disabled", () => { + const PropertySection = require("./PropertySection"); + const props = { + ...getDefaultProps(), + disabled: () => true, + disabledHelpText: "This section is disabled because..." + }; + + const { getByTestId } = render(); + + expect(getByTestId("disabled-tooltip")).toBeTruthy(); + expect(getByTestId("disabled-tooltip").textContent).toBe("This section is disabled because..."); + }); + + it("should not show disabled help text when section is not disabled", () => { + const PropertySection = require("./PropertySection"); + const props = { + ...getDefaultProps(), + disabled: () => false, + disabledHelpText: "This section is disabled because..." + }; + + const { queryByTestId } = render(); + + expect(queryByTestId("disabled-tooltip")).toBeFalsy(); + }); + + it("clicking on section title should trigger toggle when not disabled", () => { + const PropertySection = require("./PropertySection"); + const { getByTestId } = render(); + + const sectionTitle = getByTestId("section-title"); + sectionTitle.click(); + + expect(mockOnToggle).toHaveBeenCalled(); + }); + + it("clicking on section title should not trigger toggle when disabled", () => { + const PropertySection = require("./PropertySection"); + const props = { + ...getDefaultProps(), + disabled: () => true + }; + + const { getByTestId } = render(); + + const sectionTitle = getByTestId("section-title"); + sectionTitle.click(); + + expect(mockOnToggle).not.toHaveBeenCalled(); + }); +}); \ No newline at end of file From 845406a08a93ef55176bde1e353aea9f0fcb5563 Mon Sep 17 00:00:00 2001 From: jacquesikot Date: Wed, 26 Mar 2025 15:31:45 +0100 Subject: [PATCH 06/32] fix: disabled prop type mismatch between PropertyPaneControlConfig and LazyCodeEditor --- .../propertyControls/CodeEditorControl.tsx | 44 +++++++++++-------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/app/client/src/components/propertyControls/CodeEditorControl.tsx b/app/client/src/components/propertyControls/CodeEditorControl.tsx index 19f47fb09745..047cd0f3166c 100644 --- a/app/client/src/components/propertyControls/CodeEditorControl.tsx +++ b/app/client/src/components/propertyControls/CodeEditorControl.tsx @@ -1,50 +1,56 @@ -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() { const { controlConfig, dataTreePath, + disabled, evaluatedValue, expected, propertyValue, useValidationMessage, } = this.props; - const props: Partial = {}; - - if (dataTreePath) props.dataTreePath = dataTreePath; - - if (evaluatedValue) props.evaluatedValue = evaluatedValue; - - if (expected) props.expected = expected; - + // PropertyPaneControlConfig's disabled is a function (props: any, propertyPath: string) => boolean + // while LazyCodeEditor expects a boolean. Convert function to boolean result. + const isReadOnly = typeof disabled === "function" + ? disabled(this.props.widgetProperties, this.props.propertyName) + : !!disabled; + + const maxHeight = controlConfig?.maxHeight ? + String(controlConfig.maxHeight) : undefined; + return ( ); From 5a57c17c8bd026e93c0c32d9c85e8e2c31215883 Mon Sep 17 00:00:00 2001 From: jacquesikot Date: Wed, 26 Mar 2025 16:13:55 +0100 Subject: [PATCH 07/32] refactor: Update imports and context usage in property controls to fix cyclic dependency --- .../propertyControls/InputTextControl.tsx | 16 +++++++------- .../TableCustomSortControl.tsx | 22 +++++++++---------- .../Editor/PropertyPane/PropertyControl.tsx | 2 +- .../PropertyPane/PropertyPaneContexts.tsx | 6 +++++ .../Editor/PropertyPane/PropertySection.tsx | 8 ++----- 5 files changed, 28 insertions(+), 26 deletions(-) create mode 100644 app/client/src/pages/Editor/PropertyPane/PropertyPaneContexts.tsx 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/pages/Editor/PropertyPane/PropertyControl.tsx b/app/client/src/pages/Editor/PropertyPane/PropertyControl.tsx index c103a84553ca..62cee38505f1 100644 --- a/app/client/src/pages/Editor/PropertyPane/PropertyControl.tsx +++ b/app/client/src/pages/Editor/PropertyPane/PropertyControl.tsx @@ -64,7 +64,7 @@ import { useFeatureFlag } from "utils/hooks/useFeatureFlag"; import { FEATURE_FLAG } from "ee/entities/FeatureFlag"; import { savePropertyInSessionStorageIfRequired } from "./helpers"; import { getParentWidget } from "selectors/widgetSelectors"; -import { DisabledContext } from "./PropertySection"; +import { DisabledContext } from "./PropertyPaneContexts"; const ResetIcon = importSvg( async () => import("assets/icons/control/undo_2.svg"), diff --git a/app/client/src/pages/Editor/PropertyPane/PropertyPaneContexts.tsx b/app/client/src/pages/Editor/PropertyPane/PropertyPaneContexts.tsx new file mode 100644 index 000000000000..9e0149979337 --- /dev/null +++ b/app/client/src/pages/Editor/PropertyPane/PropertyPaneContexts.tsx @@ -0,0 +1,6 @@ +import { createContext } from "react"; + +// Context is being provided to re-render anything that subscribes to this context on open and close +export const CollapseContext = createContext(false); +// Context for propagating the disabled state from section to child controls +export const DisabledContext = createContext(false); \ No newline at end of file diff --git a/app/client/src/pages/Editor/PropertyPane/PropertySection.tsx b/app/client/src/pages/Editor/PropertyPane/PropertySection.tsx index 279ebc8b0901..3994ccd6ff7b 100644 --- a/app/client/src/pages/Editor/PropertyPane/PropertySection.tsx +++ b/app/client/src/pages/Editor/PropertyPane/PropertySection.tsx @@ -1,10 +1,9 @@ import { Classes } from "@blueprintjs/core"; -import type { ReactNode, Context } from "react"; +import type { ReactNode } from "react"; import React, { memo, useState, useEffect, - createContext, useCallback, } from "react"; import { Collapse } from "@blueprintjs/core"; @@ -21,6 +20,7 @@ import localStorage from "utils/localStorage"; import { WIDGET_ID_SHOW_WALKTHROUGH } from "constants/WidgetConstants"; import { PROPERTY_PANE_ID } from "components/editorComponents/PropertyPaneSidebar"; import { getWidgetPropsForPropertyPane } from "selectors/propertyPaneSelectors"; +import { CollapseContext, DisabledContext } from "./PropertyPaneContexts"; const TagContainer = styled.div``; @@ -97,10 +97,6 @@ const areEqual = (prev: PropertySectionProps, next: PropertySectionProps) => { return prev.id === next.id && prev.childrenId === next.childrenId; }; -// Context is being provided to re-render anything that subscribes to this context on open and close -export const CollapseContext: Context = createContext(false); -export const DisabledContext: Context = createContext(false); - export const PropertySection = memo((props: PropertySectionProps) => { const dispatch = useDispatch(); const currentWidgetId = useSelector(getCurrentWidgetId); From d8cba00047247b92b22a3e52e394b8d1c2653440 Mon Sep 17 00:00:00 2001 From: jacquesikot Date: Thu, 27 Mar 2025 02:08:02 +0100 Subject: [PATCH 08/32] refactor: Clean up formatting and improve readability in property control components --- .../propertyControls/CodeEditorControl.tsx | 18 +- .../PropertyPane/PropertyControl.test.tsx | 41 +- .../Editor/PropertyPane/PropertyControl.tsx | 386 +++++++++--------- .../PropertyPane/PropertyPaneContexts.tsx | 2 +- .../PropertyPane/PropertySection.test.tsx | 57 +-- .../Editor/PropertyPane/PropertySection.tsx | 105 +++-- 6 files changed, 313 insertions(+), 296 deletions(-) diff --git a/app/client/src/components/propertyControls/CodeEditorControl.tsx b/app/client/src/components/propertyControls/CodeEditorControl.tsx index 047cd0f3166c..80a6d828fc74 100644 --- a/app/client/src/components/propertyControls/CodeEditorControl.tsx +++ b/app/client/src/components/propertyControls/CodeEditorControl.tsx @@ -27,13 +27,15 @@ class CodeEditorControl extends BaseControl { // PropertyPaneControlConfig's disabled is a function (props: any, propertyPath: string) => boolean // while LazyCodeEditor expects a boolean. Convert function to boolean result. - const isReadOnly = typeof disabled === "function" - ? disabled(this.props.widgetProperties, this.props.propertyName) - : !!disabled; - - const maxHeight = controlConfig?.maxHeight ? - String(controlConfig.maxHeight) : undefined; - + const isReadOnly = + typeof disabled === "function" + ? disabled(this.props.widgetProperties, this.props.propertyName) + : !!disabled; + + const maxHeight = controlConfig?.maxHeight + ? String(controlConfig.maxHeight) + : undefined; + return ( { positionCursorInsideBinding size={EditorSize.EXTENDED} tabBehaviour={TabBehaviour.INDENT} - theme={EditorTheme.LIGHT} + theme={EditorTheme.LIGHT} useValidationMessage={useValidationMessage} AIAssisted /> diff --git a/app/client/src/pages/Editor/PropertyPane/PropertyControl.test.tsx b/app/client/src/pages/Editor/PropertyPane/PropertyControl.test.tsx index fe0c2826077e..e2e5e5047e6f 100644 --- a/app/client/src/pages/Editor/PropertyPane/PropertyControl.test.tsx +++ b/app/client/src/pages/Editor/PropertyPane/PropertyControl.test.tsx @@ -4,14 +4,19 @@ import { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig import type { IPanelProps } from "@blueprintjs/core"; const MockPropertyControl = (props: any) => { - const isDisabled = props.disabled ? props.disabled(props.widgetProperties, props.propertyName) : false; - + const isDisabled = props.disabled + ? props.disabled(props.widgetProperties, props.propertyName) + : false; + return ( -
+
- @@ -22,7 +27,9 @@ const MockPropertyControl = (props: any) => { ); }; -jest.mock("./PropertyControl", () => (props: any) => ); +jest.mock("./PropertyControl", () => (props: any) => ( + +)); describe("PropertyControl", () => { const mockPanel: IPanelProps = { @@ -43,15 +50,17 @@ describe("PropertyControl", () => { widgetId: "test-widget", widgetName: "TestWidget", type: "CONTAINER_WIDGET", - testProperty: "test value" - } + testProperty: "test value", + }, }; it("should render property control normally when not disabled", () => { const PropertyControl = require("./PropertyControl"); const { getByTestId } = render(); - - expect((getByTestId("property-input") as HTMLInputElement).disabled).toBe(false); + + expect((getByTestId("property-input") as HTMLInputElement).disabled).toBe( + false, + ); expect(getByTestId("t--property-control-wrapper").className).toBe(""); }); @@ -67,7 +76,9 @@ describe("PropertyControl", () => { 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("property-input") as HTMLInputElement).disabled).toBe(true); + expect((getByTestId("property-input") as HTMLInputElement).disabled).toBe( + true, + ); }); it("should show disabled help text when property is disabled", () => { @@ -81,7 +92,9 @@ describe("PropertyControl", () => { const { getByTestId } = render(); expect(getByTestId("disabled-help-text")).toBeTruthy(); - expect(getByTestId("disabled-help-text").textContent).toBe("This property is disabled because..."); + expect(getByTestId("disabled-help-text").textContent).toBe( + "This property is disabled because...", + ); }); it("should not show disabled help text when property is not disabled", () => { @@ -96,4 +109,4 @@ describe("PropertyControl", () => { expect(queryByTestId("disabled-help-text")).toBeFalsy(); }); -}); \ No newline at end of file +}); diff --git a/app/client/src/pages/Editor/PropertyPane/PropertyControl.tsx b/app/client/src/pages/Editor/PropertyPane/PropertyControl.tsx index 62cee38505f1..82de447b67f2 100644 --- a/app/client/src/pages/Editor/PropertyPane/PropertyControl.tsx +++ b/app/client/src/pages/Editor/PropertyPane/PropertyControl.tsx @@ -1,4 +1,11 @@ -import React, { memo, useCallback, useContext, useEffect, useRef, useState } from "react"; +import React, { + memo, + useCallback, + useContext, + useEffect, + useRef, + useState, +} from "react"; import _, { get, isFunction, merge } from "lodash"; import equal from "fast-deep-equal/es6"; import * as log from "loglevel"; @@ -108,7 +115,8 @@ const PropertyControl = memo((props: Props) => { getParentWidget(state, widgetProperties.widgetId), ); - const isControlDisabled = props.disabled && props.disabled(widgetProperties, props.propertyName); + const isControlDisabled = + props.disabled && props.disabled(widgetProperties, props.propertyName); const isDisabled = isSectionDisabled || !!isControlDisabled; @@ -924,214 +932,214 @@ const PropertyControl = memo((props: Props) => { try { return ( - {isRenaming && config.controlConfig?.allowEdit ? ( -
-
- { - const value = e.target.value; - - // Non-word characters are replaced with underscores for valid property naming - setEditedName(value.split(/\W+/).join("_")); - }} - onKeyDown={(e) => { - if (e.key === "Enter") { - onEditSave(); - } else if (e.key === "Escape") { - resetEditing(); - } - }} - placeholder="Enter label" - value={editedName} - /> -
-
-
-
-
-
- ) : ( -
- - - {isConvertible && ( - - - - toggleDynamicProperty( - propertyName, - isDynamic, - controlMethods?.shouldValidateValueOnDynamicPropertyOff( - config, - propertyValue, - ), - ) - } - size={experimentalJSToggle ? "md" : "sm"} - /> - - - )} - {isPropertyDeviatedFromTheme && ( - <> - - - - - - )} - -
- {config.controlConfig?.allowEdit && ( + className={`t--property-control-wrapper t--property-control-${className} group relative ${isDisabled ? "cursor-not-allowed opacity-50" : ""}`} + data-guided-tour-iid={propertyName} + id={uniqId} + key={config.id} + onFocus={handleOnFocus} + orientation={ + config.controlType === "SWITCH" && !isDynamic + ? "HORIZONTAL" + : "VERTICAL" + } + ref={controlRef} + > + {isRenaming && config.controlConfig?.allowEdit ? ( +
+
+ { + const value = e.target.value; + + // Non-word characters are replaced with underscores for valid property naming + setEditedName(value.split(/\W+/).join("_")); + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + onEditSave(); + } else if (e.key === "Escape") { + resetEditing(); + } + }} + placeholder="Enter label" + value={editedName} + /> +
+
+
+
+ ) : ( +
+ + - )} + {isConvertible && ( + + + + toggleDynamicProperty( + propertyName, + isDynamic, + controlMethods?.shouldValidateValueOnDynamicPropertyOff( + config, + propertyValue, + ), + ) + } + size={experimentalJSToggle ? "md" : "sm"} + /> + + + )} + {isPropertyDeviatedFromTheme && ( + <> + + + + + + )} + +
+ {config.controlConfig?.allowEdit && ( +
-
- )} - {PropertyControlFactory.createControl( + )} + {PropertyControlFactory.createControl( config, { onPropertyChange: onPropertyChange, @@ -1148,9 +1156,9 @@ const PropertyControl = memo((props: Props) => { hideEvaluatedValue(), props.isSearchResult, )} - - - ) + + + ); } catch (e) { log.error(e); diff --git a/app/client/src/pages/Editor/PropertyPane/PropertyPaneContexts.tsx b/app/client/src/pages/Editor/PropertyPane/PropertyPaneContexts.tsx index 9e0149979337..f323c3f59568 100644 --- a/app/client/src/pages/Editor/PropertyPane/PropertyPaneContexts.tsx +++ b/app/client/src/pages/Editor/PropertyPane/PropertyPaneContexts.tsx @@ -3,4 +3,4 @@ import { createContext } from "react"; // Context is being provided to re-render anything that subscribes to this context on open and close export const CollapseContext = createContext(false); // Context for propagating the disabled state from section to child controls -export const DisabledContext = createContext(false); \ No newline at end of file +export const DisabledContext = createContext(false); diff --git a/app/client/src/pages/Editor/PropertyPane/PropertySection.test.tsx b/app/client/src/pages/Editor/PropertyPane/PropertySection.test.tsx index 9e8411be3108..1713cb042833 100644 --- a/app/client/src/pages/Editor/PropertyPane/PropertySection.test.tsx +++ b/app/client/src/pages/Editor/PropertyPane/PropertySection.test.tsx @@ -1,16 +1,17 @@ import React from "react"; import { render } from "@testing-library/react"; - const MockPropertySection = (props: any) => { - const isSectionDisabled = props.disabled && props.disabled(props.widgetProps, props.propertyPath || ""); - + const isSectionDisabled = + props.disabled && + props.disabled(props.widgetProps, props.propertyPath || ""); + return ( -
-
{ > {props.name} {props.children && ( -
- {props.children} -
+
{props.children}
)}
@@ -32,11 +31,13 @@ const MockPropertySection = (props: any) => { ); }; -jest.mock("./PropertySection", () => (props: any) => ); +jest.mock("./PropertySection", () => (props: any) => ( + +)); describe("PropertySection", () => { const mockOnToggle = jest.fn(); - + const getDefaultProps = () => ({ id: "test-section", name: "Test Section", @@ -47,11 +48,11 @@ describe("PropertySection", () => { widgetId: "test-widget", widgetName: "TestWidget", type: "CONTAINER_WIDGET", - testProperty: "test value" + testProperty: "test value", }, - onToggle: mockOnToggle + onToggle: mockOnToggle, }); - + beforeEach(() => { jest.clearAllMocks(); }); @@ -59,11 +60,11 @@ describe("PropertySection", () => { it("should render section normally when not disabled", () => { const PropertySection = require("./PropertySection"); const { getByTestId } = render(); - + const wrapper = getByTestId("t--property-pane-section-wrapper"); expect(wrapper.className).not.toContain("cursor-not-allowed"); expect(wrapper.className).not.toContain("opacity-50"); - + const sectionTitle = getByTestId("section-title"); expect(sectionTitle.className).toContain("cursor-pointer"); }); @@ -72,7 +73,7 @@ describe("PropertySection", () => { const PropertySection = require("./PropertySection"); const props = { ...getDefaultProps(), - disabled: () => true + disabled: () => true, }; const { getByTestId } = render(); @@ -80,7 +81,7 @@ describe("PropertySection", () => { const wrapper = getByTestId("t--property-pane-section-wrapper"); expect(wrapper.classList.contains("cursor-not-allowed")).toBe(true); expect(wrapper.classList.contains("opacity-50")).toBe(true); - + const sectionTitle = getByTestId("section-title"); expect(sectionTitle.className).toContain("cursor-default"); }); @@ -90,13 +91,15 @@ describe("PropertySection", () => { const props = { ...getDefaultProps(), disabled: () => true, - disabledHelpText: "This section is disabled because..." + disabledHelpText: "This section is disabled because...", }; const { getByTestId } = render(); expect(getByTestId("disabled-tooltip")).toBeTruthy(); - expect(getByTestId("disabled-tooltip").textContent).toBe("This section is disabled because..."); + expect(getByTestId("disabled-tooltip").textContent).toBe( + "This section is disabled because...", + ); }); it("should not show disabled help text when section is not disabled", () => { @@ -104,7 +107,7 @@ describe("PropertySection", () => { const props = { ...getDefaultProps(), disabled: () => false, - disabledHelpText: "This section is disabled because..." + disabledHelpText: "This section is disabled because...", }; const { queryByTestId } = render(); @@ -115,10 +118,10 @@ describe("PropertySection", () => { it("clicking on section title should trigger toggle when not disabled", () => { const PropertySection = require("./PropertySection"); const { getByTestId } = render(); - + const sectionTitle = getByTestId("section-title"); sectionTitle.click(); - + expect(mockOnToggle).toHaveBeenCalled(); }); @@ -126,14 +129,14 @@ describe("PropertySection", () => { const PropertySection = require("./PropertySection"); const props = { ...getDefaultProps(), - disabled: () => true + disabled: () => true, }; const { getByTestId } = render(); - + const sectionTitle = getByTestId("section-title"); sectionTitle.click(); - + expect(mockOnToggle).not.toHaveBeenCalled(); }); -}); \ No newline at end of file +}); diff --git a/app/client/src/pages/Editor/PropertyPane/PropertySection.tsx b/app/client/src/pages/Editor/PropertyPane/PropertySection.tsx index 3994ccd6ff7b..0d5d0993ae6d 100644 --- a/app/client/src/pages/Editor/PropertyPane/PropertySection.tsx +++ b/app/client/src/pages/Editor/PropertyPane/PropertySection.tsx @@ -1,11 +1,6 @@ import { Classes } from "@blueprintjs/core"; import type { ReactNode } from "react"; -import React, { - memo, - useState, - useEffect, - useCallback, -} from "react"; +import React, { memo, useState, useEffect, useCallback } from "react"; import { Collapse } from "@blueprintjs/core"; import styled from "styled-components"; import { Colors } from "constants/Colors"; @@ -109,9 +104,10 @@ export const PropertySection = memo((props: PropertySectionProps) => { ); const isSearchResult = props.tag !== undefined; const [isOpen, setIsOpen] = useState(!!isContextOpen); - + const widgetProps = useSelector(getWidgetPropsForPropertyPane); - const isSectionDisabled = props.disabled && props.disabled(widgetProps, props.propertyPath || ""); + const isSectionDisabled = + props.disabled && props.disabled(widgetProps, props.propertyPath || ""); const className = props.name.split(" ").join("").toLowerCase(); const connectDataClicked = useSelector(getIsOneClickBindingOptionsVisibility); @@ -182,58 +178,53 @@ export const PropertySection = memo((props: PropertySectionProps) => { const sectionContent = ( -
- {props.name} - {props.tag && ( - - + {props.name} + {props.tag && ( + + + {props.tag.toLowerCase()} + + + )} + {props.collapsible && !isSectionDisabled && ( + + )} +
+ {props.children && ( + +
- {props.tag.toLowerCase()} - - - )} - {props.collapsible && !isSectionDisabled && ( - + + {props.children} + +
+
)} -
- {props.children && ( - -
- - {props.children} - -
-
- )} - - ) - - return ( - isSectionDisabled && props.disabledHelpText ? ( - - {sectionContent} - - ) : ( - sectionContent - ) - + + ); + + return isSectionDisabled && props.disabledHelpText ? ( + {sectionContent} + ) : ( + sectionContent ); }, areEqual); From 14db8bbbf2698af4e397705d11a6dfbbf6447674 Mon Sep 17 00:00:00 2001 From: jacquesikot Date: Thu, 27 Mar 2025 09:26:12 +0100 Subject: [PATCH 09/32] refactor: Update imports in PropertySection component --- .../Editor/PropertyPane/PropertySection.tsx | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/app/client/src/pages/Editor/PropertyPane/PropertySection.tsx b/app/client/src/pages/Editor/PropertyPane/PropertySection.tsx index 0d5d0993ae6d..96056f381b47 100644 --- a/app/client/src/pages/Editor/PropertyPane/PropertySection.tsx +++ b/app/client/src/pages/Editor/PropertyPane/PropertySection.tsx @@ -1,21 +1,22 @@ -import { Classes } from "@blueprintjs/core"; -import type { ReactNode } from "react"; -import React, { memo, useState, useEffect, useCallback } from "react"; -import { Collapse } from "@blueprintjs/core"; -import styled from "styled-components"; -import { Colors } from "constants/Colors"; import { Icon, Tag, Tooltip } from "@appsmith/ads"; +import { Classes, Collapse } from "@blueprintjs/core"; +import { setPropertySectionState } from "actions/propertyPaneActions"; +import { PROPERTY_PANE_ID } from "components/editorComponents/PropertyPaneSidebar"; +import { Colors } from "constants/Colors"; +import { WIDGET_ID_SHOW_WALKTHROUGH } from "constants/WidgetConstants"; import type { AppState } from "ee/reducers"; +import type { ReactNode } from "react"; +import React, { memo, useCallback, useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { getPropertySectionState } from "selectors/editorContextSelectors"; -import { getCurrentWidgetId } from "selectors/propertyPaneSelectors"; -import { setPropertySectionState } from "actions/propertyPaneActions"; import { getIsOneClickBindingOptionsVisibility } from "selectors/oneClickBindingSelectors"; +import { + getCurrentWidgetId, + getWidgetPropsForPropertyPane, +} from "selectors/propertyPaneSelectors"; +import styled from "styled-components"; import localStorage from "utils/localStorage"; -import { WIDGET_ID_SHOW_WALKTHROUGH } from "constants/WidgetConstants"; -import { PROPERTY_PANE_ID } from "components/editorComponents/PropertyPaneSidebar"; -import { getWidgetPropsForPropertyPane } from "selectors/propertyPaneSelectors"; -import { CollapseContext, DisabledContext } from "./PropertyPaneContexts"; +import { CollapseContext } from "./PropertyPaneContexts"; const TagContainer = styled.div``; From 353635286f7adbebf2677271cf306c4484873f65 Mon Sep 17 00:00:00 2001 From: jacquesikot Date: Thu, 27 Mar 2025 10:56:07 +0100 Subject: [PATCH 10/32] fix: lint error --- .../propertyControls/CodeEditorControl.tsx | 2 +- .../PropertyPane/PropertyControl.test.tsx | 54 +++++++++++-------- .../PropertyControlsGenerator.tsx | 2 +- .../PropertyPane/PropertySection.test.tsx | 27 +++++++--- 4 files changed, 52 insertions(+), 33 deletions(-) diff --git a/app/client/src/components/propertyControls/CodeEditorControl.tsx b/app/client/src/components/propertyControls/CodeEditorControl.tsx index 80a6d828fc74..bdaf22295065 100644 --- a/app/client/src/components/propertyControls/CodeEditorControl.tsx +++ b/app/client/src/components/propertyControls/CodeEditorControl.tsx @@ -38,6 +38,7 @@ class CodeEditorControl extends BaseControl { return ( { tabBehaviour={TabBehaviour.INDENT} theme={EditorTheme.LIGHT} useValidationMessage={useValidationMessage} - AIAssisted /> ); } diff --git a/app/client/src/pages/Editor/PropertyPane/PropertyControl.test.tsx b/app/client/src/pages/Editor/PropertyPane/PropertyControl.test.tsx index e2e5e5047e6f..b30c8bebf133 100644 --- a/app/client/src/pages/Editor/PropertyPane/PropertyControl.test.tsx +++ b/app/client/src/pages/Editor/PropertyPane/PropertyControl.test.tsx @@ -2,8 +2,19 @@ import React from "react"; import { render } from "@testing-library/react"; import { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig"; import type { IPanelProps } from "@blueprintjs/core"; - -const MockPropertyControl = (props: any) => { +import type { WidgetProps } from "widgets/BaseWidget"; +import PropertyControl from "./PropertyControl"; +import type { EnhancementFns } from "selectors/widgetEnhancementSelectors"; + +interface MockPropertyControlProps { + disabled?: (widgetProperties: WidgetProps, propertyName: string) => boolean; + disabledHelpText?: string; + label: string; + propertyName: string; + widgetProperties: WidgetProps; +} + +const MockPropertyControl = (props: MockPropertyControlProps) => { const isDisabled = props.disabled ? props.disabled(props.widgetProperties, props.propertyName) : false; @@ -18,16 +29,16 @@ const MockPropertyControl = (props: any) => { type="text" role="textbox" disabled={isDisabled} - data-testid="property-input" + data-testid="t--property-input" /> {isDisabled && props.disabledHelpText && ( -
{props.disabledHelpText}
+
{props.disabledHelpText}
)}
); }; -jest.mock("./PropertyControl", () => (props: any) => ( +jest.mock("./PropertyControl", () => (props: MockPropertyControlProps) => ( )); @@ -38,34 +49,33 @@ describe("PropertyControl", () => { }; const defaultProps = { - propertyName: "testProperty", - label: "Test Label", controlType: "INPUT_TEXT", + enhancements: undefined as EnhancementFns | undefined, isBindProperty: true, + isSearchResult: false, isTriggerProperty: false, + label: "Test Label", panel: mockPanel, + propertyName: "testProperty", theme: EditorTheme.LIGHT, - isSearchResult: false, widgetProperties: { + testProperty: "test value", + type: "CONTAINER_WIDGET", widgetId: "test-widget", widgetName: "TestWidget", - type: "CONTAINER_WIDGET", - testProperty: "test value", }, }; it("should render property control normally when not disabled", () => { - const PropertyControl = require("./PropertyControl"); const { getByTestId } = render(); - expect((getByTestId("property-input") as HTMLInputElement).disabled).toBe( - false, - ); + 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 PropertyControl = require("./PropertyControl"); const disabledProps = { ...defaultProps, disabled: () => true, @@ -76,13 +86,12 @@ describe("PropertyControl", () => { 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("property-input") as HTMLInputElement).disabled).toBe( - true, - ); + expect( + (getByTestId("t--property-input") as HTMLInputElement).disabled, + ).toBe(true); }); it("should show disabled help text when property is disabled", () => { - const PropertyControl = require("./PropertyControl"); const disabledProps = { ...defaultProps, disabled: () => true, @@ -91,14 +100,13 @@ describe("PropertyControl", () => { const { getByTestId } = render(); - expect(getByTestId("disabled-help-text")).toBeTruthy(); - expect(getByTestId("disabled-help-text").textContent).toBe( + 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 PropertyControl = require("./PropertyControl"); const props = { ...defaultProps, disabled: () => false, @@ -107,6 +115,6 @@ describe("PropertyControl", () => { const { queryByTestId } = render(); - expect(queryByTestId("disabled-help-text")).toBeFalsy(); + expect(queryByTestId("t--disabled-help-text")).toBeFalsy(); }); }); diff --git a/app/client/src/pages/Editor/PropertyPane/PropertyControlsGenerator.tsx b/app/client/src/pages/Editor/PropertyPane/PropertyControlsGenerator.tsx index 32f000bbb1c5..c32f8cd668df 100644 --- a/app/client/src/pages/Editor/PropertyPane/PropertyControlsGenerator.tsx +++ b/app/client/src/pages/Editor/PropertyPane/PropertyControlsGenerator.tsx @@ -59,9 +59,9 @@ const generatePropertyControl = (